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 : wrapsWhen 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
SmsSenderinterface). - 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.
Click Run TS to execute. First run downloads Babel (~400 KB, cached after that).
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).
Click Run Go to execute. Runs via the Go Playground API.
Tradeoffs
| Pro | Con |
|---|---|
| Integrate legacy or third-party code without touching it | Adds a translation layer with its own maintenance burden |
| Client code stays clean, depends only on the target interface | Data conversion (cents/dollars, codes/enums) can introduce bugs |
| Easy to test: stub the adaptee in unit tests | Class adapter (multiple inheritance) is messy in most languages |
| Standard way to wrap an SDK you do not own | If the adaptee API changes, the adapter must change too |
Gotchas
- Round money conversions consistently.
amount * 100in floating point can produce 2998 instead of 2999 for certain inputs. Usemath.Roundin 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
isinstancechecks, 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
Chargeorpay.
References
- Design Patterns: Adapter, GoF, the canonical definition with class and object adapter variants
- Refactoring Guru: Adapter, diagrams and examples in multiple languages
- SourceMaking: Adapter, additional motivation and known uses
- Go interfaces and implicit satisfaction, the language property that makes object adapters idiomatic in Go
Related topics
- 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