Marcelo Pastorino, Software Developer

Adapter Pattern

A structural design pattern that wraps an incompatible interface in one the client expects, letting otherwise unrelated types work together.

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.
Adapter reconciling a client with an incompatible adaptee

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 gateway
from 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 gateway

Object 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.

See also

  • Decorator Pattern, A structural design pattern that wraps an object to add behavior while keeping the same interface, so responsibilities can be composed in layers at runtime.
  • Facade Pattern, A structural design pattern that exposes a single, simplified interface over a complex subsystem, hiding how its parts cooperate.
  • Proxy Pattern, A structural design pattern that provides a surrogate with the same interface as a real object, controlling access, deferring work, or adding indirection without changing what the client sees.
  • Strategy Pattern, A behavioral design pattern that encapsulates interchangeable algorithms behind a common interface, selected at runtime.
  • SOLID Principles, Five object-oriented design principles for maintainable, change-tolerant code.

On Wikipedia