Adapter Pattern — Making Incompatible Interfaces Work Together
What You'll Learn
You will learn how the Adapter pattern bridges mismatched interfaces without modifying existing code, and how to choose between class adapters (using inheritance) and object adapters (using composition).
Why It Matters
Legacy systems, third-party libraries, and remote services rarely expose the exact interface your application expects. Rewriting them is impractical. Adapter lets you wrap the incompatible component in a thin translation layer, preserving both the legacy code and your Clean Architecture. The alternative — modifying the third-party library's source code or writing complex conditional branches in your business logic — is either impossible or unsustainable.
Real-World Use
Payment gateways expose different APIs — Stripe, PayPal, Square — but your checkout service should call a single interface. DodaTech's integration hub uses adapters to normalise dozens of external APIs into a uniform internal contract. When a new payment provider enters the market, the integration team writes one Adapter class and registers it; the checkout code never changes.
The Pattern
A Target interface defines what the client expects. The Adaptee has the incompatible interface. The Adapter sits in between, translating Target calls into Adaptee operations.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> dict:
pass
class StripeAPI:
def create_charge(self, total: int, cur: str) -> dict:
return {"status": "ok", "transaction_id": "stripe_123", "amount": total / 100}
class StripeAdapter(PaymentProcessor):
def __init__(self, api: StripeAPI):
self._api = api
def charge(self, amount: float, currency: str) -> dict:
cents = int(amount * 100)
return self._api.create_charge(cents, currency)
class PayPalAPI:
def send_payment(self, amt: float, curr: str) -> dict:
return {"status": "success", "txn": "paypal_456", "amount": amt}
class PayPalAdapter(PaymentProcessor):
def __init__(self, api: PayPalAPI):
self._api = api
def charge(self, amount: float, currency: str) -> dict:
return self._api.send_payment(amount, currency)
processors = [
StripeAdapter(StripeAPI()),
PayPalAdapter(PayPalAPI()),
]
for p in processors:
result = p.charge(19.99, "USD")
print(result["status"], result.get("transaction_id") or result.get("txn"))
ok stripe_123
success paypal_456
Structure
classDiagram
class Target {
<>
+request()
}
class Client {
+operation(target: Target)
}
class Adapter {
+request()
}
class Adaptee {
+specificRequest()
}
Client --> Target : depends on
Adapter ..|> Target
Adapter --> Adaptee : delegates
Real-World Usage
- Java
Arrays.asList()— adapts an array to theListinterface without copying. - Python
file-like objects —StringIOadapts strings to the file interface. - .NET
System.Data.Common.DbDataAdapter— fills aDataSetfrom any data source. - CSS vendor prefixes — a real-world analogy where you write standard properties and prefixes adapt behaviour for older browsers.
Related Patterns
- Bridge separates abstraction from implementation; Adapter makes unrelated interfaces work together.
- Decorator adds responsibilities without changing interface; Adapter changes the interface.
- Facade provides a simpler interface; Adapter provides the exact interface the client expects.
- Proxy controls access; Adapter changes interface.
Pros and Cons
| Pros | Cons |
|---|---|
| Enables integration of incompatible code | Adds indirection, which can complicate debugging |
| Follows the Open/Closed Principle — no modification of existing classes | Can become unwieldy when many adapters are needed |
| Single Responsibility Principle — adaptation logic is isolated | Performance overhead from the delegation layer |
| Can be introduced gradually into a legacy codebase | Class adapters (multiple inheritance) can introduce ambiguity |
The code reveals the essence of the pattern: the StripeAdapter and PayPalAdapter both implement the PaymentProcessor interface while internally using completely different APIs. The StripeAdapter converts dollars to cents (because Stripe works in smallest currency units), while PayPalAdapter passes the amount directly. The client (processors loop) treats both identically.
Practice Questions
- When would you choose a two-way Adapter that both converts and is convertible? Consider migrating from one library version to another.
- How does the Adapter pattern relate to the Law Of Demeter? Does an Adapter help or hurt adherence?
- Implement an Adapter that makes a modern logging framework work with a legacy
System.outinterface. - What tests would you write to verify that an Adapter correctly translates all edge cases — null values, special characters, currency conversions?
Challenge
Add a third payment gateway (e.g., Square) that uses yet another interface signature. Write only the Adapter and verify it works with the existing checkout loop without modifications.
Real-World Task
Identify a third-party library in your project that has a different interface than the rest of your codebase. Write an Adapter for it and use DodaTech's interface dependency graph to confirm that the coupling to the library is now isolated to the Adapter.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro