Skip to content

Adapter Pattern

The problem

You have a class that does something useful, but its interface does not match what your application expects. Maybe it is a legacy payment gateway that takes cents as integers and returns error codes, while your new checkout code expects decimal amounts and boolean results. Maybe it is a third-party SDK you cannot modify. Changing the caller is invasive; changing the library is impossible.

The Adapter pattern wraps the incompatible class in a new object that implements the interface your code expects. The adapter translates calls in both directions: incoming method signatures from your target interface get converted into whatever the underlying system (the adaptee) requires, and return values get translated back. Neither the caller nor the adaptee changes. The GoF book describes two forms: a class adapter, which uses multiple inheritance to extend both the target and adaptee simultaneously, and an object adapter, which holds the adaptee as a field. Class adapters are rarely practical in TypeScript, Python, or Go. Every example here uses the object adapter.

Structure

classDiagram
class PaymentProcessor {
<<interface>>
+charge(amount, currency) boolean
+refund(transactionId) boolean
}
class LegacyPaymentGateway {
+makePayment(cents, currencyCode) int
+reverseTransaction(id) boolean
}
class LegacyPaymentAdapter {
-gateway: LegacyPaymentGateway
+charge(amount, currency) boolean
+refund(transactionId) boolean
}
PaymentProcessor <|-- LegacyPaymentAdapter
LegacyPaymentAdapter --> LegacyPaymentGateway : wraps

When to use

  • You need to integrate a library or legacy class whose interface does not match what your application expects, and you cannot modify either side.
  • You want to insulate your application from a third-party API so that swapping the underlying provider only requires a new adapter, not changes throughout the codebase.
  • You are standardizing multiple external services behind a single interface (for example, adapting three different SMS providers to one SmsSender interface).
  • You want to test code that depends on an external system by substituting a stub adapter.

Implementation

Each example wraps a LegacyPaymentGateway (takes cents as an integer, returns a numeric result code) behind a PaymentProcessor interface (takes decimal amounts, returns booleans). The adapter converts between the two representations so client code never sees the legacy details. Python uses abc.ABC with @abstractmethod to make the target interface explicit and enable isinstance checks. Go satisfies interfaces implicitly: LegacyPaymentAdapter implements PaymentProcessor because its method signatures match, and a compile-time blank assignment catches any drift.

idle
Click Run TS to execute. First run downloads Babel (~400 KB, cached after that).

Tradeoffs

ProCon
Integrate legacy or third-party code without touching itAdds a translation layer with its own maintenance burden
Client code stays clean, depends only on the target interfaceData conversion (cents/dollars, codes/enums) can introduce bugs
Easy to test: stub the adaptee in unit testsClass adapter (multiple inheritance) is messy in most languages
Standard way to wrap an SDK you do not ownIf the adaptee API changes, the adapter must change too

Gotchas

  • Round money conversions consistently. amount * 100 in floating point can produce 2998 instead of 2999 for certain inputs. Use math.Round in Go, round() in Python, or a dedicated decimal library for production financial code.
  • Adapter and Facade are often confused. Adapter makes two things compatible by translating between their interfaces. Facade simplifies a complex subsystem behind a convenience interface. They solve different problems and can coexist: a facade can use adapters internally.
  • In Python, the abstract base class is optional. Skip it when duck typing is sufficient, for example in a small service where there is only ever one concrete processor. Add it when you want isinstance checks, enforced method signatures, or explicit IDE support.
  • In Go, add the compile-time check var _ TargetInterface = (*AdapterType)(nil) near the adapter declaration. It costs nothing at runtime and catches method signature drift immediately when the adaptee changes.
  • Keep the adapter thin. If translation logic grows complex, extract it into a helper function that is separately testable, rather than burying it in Charge or pay.

References

  • Design Patterns, the full GoF catalog
  • Facade, simplifies a subsystem rather than bridging two incompatible interfaces
  • Proxy, also wraps an object but to control access, not translate interfaces
  • Strategy, a behavioral complement: adapters fix interface mismatches, strategies swap algorithms