The adapter pattern (also called wrapper) is a structural design pattern that converts the interface of a class into another interface the client expects. It lets two components with incompatible interfaces collaborate by placing a thin translation layer between them, without changing either side.
The problem it solves
Code that already depends on a well-defined interface often needs to use a class that does the right job but exposes the wrong shape, such as a third-party SDK, a legacy service, or a library that speaks different method names, argument formats, or units. Editing the client to accommodate each foreign interface spreads translation logic everywhere and couples your code to details that may change or be replaced.
The adapter pattern isolates that mismatch in one place. The client keeps depending on its own interface, and an adapter wraps the incompatible type and translates each call, so the foreign class can be swapped or upgraded without touching the client.
Structure
- Target, the interface the client expects and depends on.
- Adaptee, the existing class with a useful implementation but an incompatible interface.
- Adapter, implements the target interface and holds a reference to an adaptee, translating each target call into one or more adaptee calls.
- Client, works only against the target interface, unaware that an adapter sits behind it.
flowchart LR Client["Client"] -->|"depends on"| Target["Target interface"] Adapter["Adapter"] -.->|"implements"| Target Adapter -->|"translates and delegates"| Adaptee["Adaptee (incompatible interface)"]
In code
A checkout expects a PaymentProcessor that charges an amount in cents. A legacy gateway only exposes makePayment in whole dollars. The adapter implements the expected interface and translates the call. The checkout never learns about the gateway.
// Adaptee: existing class with an incompatible interface
class LegacyGateway {
makePayment(dollars) {
console.log(`Charging $${dollars.toFixed(2)} via legacy gateway`);
}
}
// Adapter: exposes the interface the client expects, translates the call
class LegacyGatewayAdapter {
constructor(gateway) {
this.gateway = gateway;
}
pay(amountInCents) {
this.gateway.makePayment(amountInCents / 100);
}
}
// Client depends only on the target interface: pay(amountInCents)
function checkout(processor, amountInCents) {
processor.pay(amountInCents);
}
checkout(new LegacyGatewayAdapter(new LegacyGateway()), 4999);
// Charging $49.99 via legacy gateway// Target: the interface the client expects
interface PaymentProcessor {
pay(amountInCents: number): void;
}
// Adaptee: existing class with an incompatible interface
class LegacyGateway {
makePayment(dollars: number): void {
console.log(`Charging $${dollars.toFixed(2)} via legacy gateway`);
}
}
// Adapter: implements the target, translates to the adaptee
class LegacyGatewayAdapter implements PaymentProcessor {
constructor(private readonly gateway: LegacyGateway) {}
pay(amountInCents: number): void {
this.gateway.makePayment(amountInCents / 100);
}
}
// Client depends only on the target interface
function checkout(processor: PaymentProcessor, amountInCents: number): void {
processor.pay(amountInCents);
}
checkout(new LegacyGatewayAdapter(new LegacyGateway()), 4999);
// Charging $49.99 via legacy gateway// Target: the interface the client expects
public interface IPaymentProcessor
{
void Pay(int amountInCents);
}
// Adaptee: existing class with an incompatible interface
public class LegacyGateway
{
public void MakePayment(decimal dollars) =>
Console.WriteLine($"Charging ${dollars:F2} via legacy gateway");
}
// Adapter: implements the target, translates to the adaptee
public class LegacyGatewayAdapter : IPaymentProcessor
{
private readonly LegacyGateway _gateway;
public LegacyGatewayAdapter(LegacyGateway gateway) => _gateway = gateway;
public void Pay(int amountInCents) => _gateway.MakePayment(amountInCents / 100m);
}
// Client depends only on the target interface
void Checkout(IPaymentProcessor processor, int amountInCents) =>
processor.Pay(amountInCents);
Checkout(new LegacyGatewayAdapter(new LegacyGateway()), 4999);
// Charging $49.99 via legacy gatewayfrom typing import Protocol
# Target: the interface the client expects
class PaymentProcessor(Protocol):
def pay(self, amount_in_cents: int) -> None: ...
# Adaptee: existing class with an incompatible interface
class LegacyGateway:
def make_payment(self, dollars: float) -> None:
print(f"Charging ${dollars:.2f} via legacy gateway")
# Adapter: exposes the target interface, translates to the adaptee
class LegacyGatewayAdapter:
def __init__(self, gateway: LegacyGateway) -> None:
self._gateway = gateway
def pay(self, amount_in_cents: int) -> None:
self._gateway.make_payment(amount_in_cents / 100)
# Client depends only on the target interface
def checkout(processor: PaymentProcessor, amount_in_cents: int) -> None:
processor.pay(amount_in_cents)
checkout(LegacyGatewayAdapter(LegacyGateway()), 4999)
# Charging $49.99 via legacy gatewayObject vs class adapters
There are two ways to build an adapter:
- Object adapter, the adapter holds a reference to the adaptee and delegates to it (the version above). It works with any subclass of the adaptee and is the default in languages that favor composition.
- Class adapter, the adapter inherits from both the target and the adaptee, overriding calls directly. It needs multiple inheritance, so it is common in C++ but not available in single-inheritance languages like Java or C#.
Object adapters are usually preferred because composition keeps the adaptee swappable and avoids the fragility of deep inheritance.
Relationship to SOLID
Adapter is an application of the SOLID principles. It upholds the dependency inversion principle because the client depends on the target abstraction, and the adapter supplies a concrete implementation on top of a foreign type. It also supports the single-responsibility principle by confining interface translation to one class, and the open/closed principle because integrating a new incompatible type means adding an adapter rather than editing the client.
Adapter vs alternatives
- Strategy pattern, shares the composition-and-delegation structure, but the intent differs: a strategy swaps interchangeable algorithms behind a stable interface, while an adapter reconciles one fixed interface with another that cannot be changed.
- Facade pattern, a facade defines a new, simpler interface over a whole subsystem, whereas an adapter makes an existing interface match one the client already requires. Facade simplifies; adapter translates.
- Decorator pattern, a decorator keeps the same interface and adds behavior; an adapter changes the interface without adding responsibilities.
- Proxy pattern, a proxy keeps the same interface and controls access to a real subject; an adapter changes the interface to reconcile incompatible types.
- Bridge, bridge is designed up front to let abstraction and implementation vary independently, while an adapter is usually applied after the fact to make incompatible code work together.
Trade-offs
- Adds a layer of indirection and extra classes; when you control both sides, changing one interface directly is simpler than wrapping it.
- Translation can be lossy or leaky when the adaptee’s semantics do not map cleanly onto the target (different error models, units, or lifecycles).
- Best when integrating third-party or legacy code you cannot modify, or when isolating your core from an interface that is likely to change or be replaced.
