Convert the interface of a class into another interface clients expect, letting classes work together that otherwise could not because of incompatible interfaces.
01
The problem Adapter solves
The problem
Your checkout code has spoken one language for two years. Every call site says provider.charge(money, card) — a Money in cents, a Card, back comes a Receipt. Forty places call it. It is tested, it is boring, it works.
Then the company signs a deal with a new payment processor. Their SDK arrives and it speaks a different language entirely: submitTransaction(pan, dollars, currencyCode), returning a TxnResult with a string status field and a Unix timestamp. Amounts are dollars as a double, not cents. Failures come back as a return value, not an exception.
You cannot change their SDK — it's a compiled artefact from another company. And you very much do not want to change forty call sites, re-derive cents from dollars in each one, and add a fresh chance to slip a decimal point in a payment path.
02
How the Adapter pattern works
Put one small class in the middle whose entire job is translation. It implements the interface your code already speaks, and it holds the incompatible object:
The adapter implements the target interface — the one your code already calls. From the outside it is indistinguishable from any other implementation.
It holds a reference to the adaptee: the class with the wrong shape, which it does not own and cannot modify.
Each method translates — names, argument order, units, error style, return shape — calls the adaptee, and translates the answer back.
Nothing else in the codebase ever imports the adaptee. The vendor's vocabulary stops at this one file.
The renaming is the easy half. Everyone remembers Adapter as "the pattern that renames methods", and that's the part a good IDE could do for you. The part that earns its keep is the semantic translation: cents to dollars, "DECLINED" to a thrown exception, epoch seconds to an Instant, null to Optional.empty(). Those conversions are exactly where bugs live, and the pattern's real value is that there is now precisely one place they can live, and that place has unit tests.
Participants. The Target is the interface your code already speaks (PaymentProvider). The Adaptee is the class with the wrong shape (NorthPaySdk). The Adapter implements the target and delegates to the adaptee, converting on the way in and on the way back. The Client only ever sees the target — which is why swapping processors later is a one-line change at the wiring site.
03
See it: call across the mismatch
Make a call and watch it get reshaped on the way through. Then remove the adapter and try the same call — this is the one simulator on the site where turning the pattern off doesn't produce a worse design, it produces code that does not compile at all.
▶ Try it — charge, refund, look up
An interactive call tracer: CheckoutService calls charge(), refund() and status() on the PaymentProvider interface, and the NorthPayAdapter translates each one into the vendor SDK's differently-named methods and dollar-based amounts. Remove the adapter and the same calls no longer type-check.
Notice what the tally says while the adapter is in place: checkout depends on exactly one type. Remove the adapter and every call fails for a different reason — wrong units, wrong method name, wrong return type. That list of failures is the adapter's job description.
04
Adapter pattern code examples
One class that speaks both languages — and the unit conversion nobody else has to remember.
// The interface our own code has always spoken.
public interface PaymentProvider {
Receipt charge(Money amount, Card card);
}
// What the vendor ships. A compiled jar — we cannot change a line of it.
public final class NorthPaySdk {
public TxnResult submitTransaction(String pan, double dollars, String currencyCode) { /* … */ }
}
public final class NorthPayAdapter implements PaymentProvider {
private final NorthPaySdk sdk; // the adaptee — HELD, not extended
public NorthPayAdapter(NorthPaySdk sdk) {
this.sdk = sdk;
}
@Override
public Receipt charge(Money amount, Card card) {
// Every translation the vendor forces on us happens here, once.
double dollars = amount.cents() / 100.0;
TxnResult result = sdk.submitTransaction(
card.pan(),
dollars,
amount.currency().code());
// Their failures are return values; ours are exceptions. Translate that too.
if (!"APPROVED".equals(result.status)) {
throw new PaymentDeclined(result.reasonText);
}
return new Receipt(result.txnRef, amount, Instant.ofEpochSecond(result.epochSeconds));
}
}
// Wiring — the only line in the codebase that knows NorthPay exists:
PaymentProvider provider = new NorthPayAdapter(new NorthPaySdk(apiKey));
// GoF also describes a CLASS adapter (`extends NorthPaySdk implements PaymentProvider`).
// Java has no multiple inheritance, the SDK class is final, and inheriting from a
// vendor type welds you to it — so the object adapter above is what people actually write.
from decimal import Decimal
class NorthPayAdapter:
"""Duck typing removes the NAME problem. It does not remove the SHAPE problem.
If the vendor's method were also called `charge` and took the same arguments,
Python would need no adapter at all — that is the honest answer, and it is why
this pattern is less common here. But the mismatch below is about units,
argument order and error style, and no amount of duck typing fixes those.
"""
def __init__(self, sdk: NorthPaySdk) -> None:
self._sdk = sdk
def charge(self, amount: Money, card: Card) -> Receipt:
result = self._sdk.submit_transaction(
pan=card.pan,
# Decimal, never float — money that round-trips through binary
# floating point is how you end up one cent short at reconciliation.
dollars=Decimal(amount.cents) / 100,
currency_code=amount.currency,
)
if result.status != "APPROVED":
raise PaymentDeclined(result.reason_text)
return Receipt(
reference=result.txn_ref,
amount=amount,
at=datetime.fromtimestamp(result.epoch_seconds, tz=timezone.utc),
)
# Declaring the target as a Protocol gives you the compile-time check that duck
# typing otherwise costs you — mypy will now tell you if the adapter drifts:
#
# class PaymentProvider(Protocol):
# def charge(self, amount: Money, card: Card) -> Receipt: ...
#include <memory>
#include <stdexcept>
struct PaymentProvider {
virtual ~PaymentProvider() = default;
virtual Receipt charge(Money amount, const Card& card) = 0;
};
// Third-party, header-only, not ours to change.
class NorthPaySdk {
public:
TxnResult submitTransaction(const std::string& pan, double dollars,
const std::string& currencyCode);
};
class NorthPayAdapter final : public PaymentProvider {
public:
explicit NorthPayAdapter(std::shared_ptr<NorthPaySdk> sdk)
: sdk_(std::move(sdk)) {}
Receipt charge(Money amount, const Card& card) override {
const double dollars = static_cast<double>(amount.cents()) / 100.0;
TxnResult r = sdk_->submitTransaction(card.pan(), dollars, amount.currencyCode());
if (r.status != "APPROVED") throw PaymentDeclined(r.reasonText);
return Receipt{r.txnRef, amount, std::chrono::sys_seconds{std::chrono::seconds{r.epochSeconds}}};
}
private:
std::shared_ptr<NorthPaySdk> sdk_;
};
// C++ is the one language where the class adapter is genuinely idiomatic, because
// private multiple inheritance exists precisely for it:
//
// class NorthPayAdapter : public PaymentProvider, private NorthPaySdk { … };
//
// The STL leans on this idea hard: std::stack and std::queue are CONTAINER
// ADAPTERS — they hold a std::deque and expose a completely different interface
// over it. Same pattern, standardised.
public interface IPaymentProvider
{
Task<Receipt> ChargeAsync(Money amount, Card card, CancellationToken ct = default);
}
public sealed class NorthPayAdapter(NorthPaySdk sdk) : IPaymentProvider
{
public async Task<Receipt> ChargeAsync(Money amount, Card card, CancellationToken ct = default)
{
// decimal, not double — the vendor chose double, and containing that
// choice inside one class is a large part of why this adapter exists.
var dollars = (double)(amount.Cents / 100m);
var result = await sdk.SubmitTransactionAsync(card.Pan, dollars, amount.Currency.Code, ct);
if (result.Status != "APPROVED")
throw new PaymentDeclinedException(result.ReasonText);
return new Receipt(result.TxnRef, amount, DateTimeOffset.FromUnixTimeSeconds(result.EpochSeconds));
}
}
// Registered once, injected everywhere as the interface:
builder.Services.AddSingleton<NorthPaySdk>();
builder.Services.AddSingleton<IPaymentProvider, NorthPayAdapter>();
// A common wrong turn: reaching for an extension method instead.
//
// public static Receipt Charge(this NorthPaySdk sdk, Money m, Card c) => …
//
// It compiles and it reads nicely, but an extension method CANNOT implement an
// interface — so NorthPaySdk still isn't an IPaymentProvider, and none of your
// existing call sites or test doubles work. You need the class.
// The target is a shape, not a declaration, so the adapter can be a plain object
// — or a factory function returning one.
export const northPayAdapter = sdk => ({
async charge(amount, card) {
// amount.cents is an integer on purpose; the vendor wants dollars.
const dollars = amount.cents / 100
const result = await sdk.submitTransaction(card.pan, dollars, amount.currency)
if (result.status !== 'APPROVED') {
throw new PaymentDeclined(result.reason_text)
}
return {
reference: result.txn_ref,
amount,
at: new Date(result.epoch_seconds * 1000), // seconds → milliseconds
}
},
})
const provider = northPayAdapter(new NorthPaySdk(apiKey))
// Two conversions above are classic JavaScript trip-hazards worth naming:
// snake_case → camelCase across the boundary, and Unix SECONDS → Date
// MILLISECONDS. Both are silent when you get them wrong: no error, just a
// receipt dated 1970 or a field that's permanently undefined.
package payments
// Go makes the constraint explicit: you cannot define a method on a type from
// another package. If northpay.SDK doesn't satisfy your interface, wrapping it
// is not one option among several — it is the only option the language gives you.
type Provider interface {
Charge(amount Money, card Card) (Receipt, error)
}
type northPayAdapter struct {
sdk *northpay.SDK
}
func NewNorthPay(sdk *northpay.SDK) Provider {
return northPayAdapter{sdk: sdk}
}
func (a northPayAdapter) Charge(amount Money, card Card) (Receipt, error) {
dollars := float64(amount.Cents) / 100
res, err := a.sdk.SubmitTransaction(card.PAN, dollars, amount.Currency)
if err != nil {
return Receipt{}, fmt.Errorf("northpay: %w", err)
}
if res.Status != "APPROVED" {
return Receipt{}, &DeclinedError{Reason: res.ReasonText}
}
return Receipt{
Ref: res.TxnRef,
Amount: amount,
At: time.Unix(res.EpochSeconds, 0).UTC(),
}, nil
}
// The standard library's neatest adapter is a function type:
//
// type HandlerFunc func(ResponseWriter, *Request)
// func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
//
// That is the whole pattern in three lines — it adapts an ordinary function to
// the http.Handler interface, which is why you can pass a func to http.Handle.
interface PaymentProvider {
charge(amount: Money, card: Card): Promise<Receipt>
}
class NorthPayAdapter implements PaymentProvider {
constructor(private readonly sdk: NorthPaySdk) {}
async charge(amount: Money, card: Card): Promise<Receipt> {
const dollars = amount.cents / 100
const result = await this.sdk.submitTransaction(card.pan, dollars, amount.currency)
if (result.status !== 'APPROVED') {
throw new PaymentDeclined(result.reason_text)
}
return {
reference: result.txn_ref,
amount,
at: new Date(result.epoch_seconds * 1000),
}
}
}
// Structural typing means `implements` is a CHECK, not a requirement — anything
// with a matching `charge` already satisfies PaymentProvider. Write it anyway:
// it moves the error from the call site to the adapter, which is where you can
// actually fix it.
// Branded types stop the unit bug the adapter exists to contain, by making the
// two numbers different types that cannot be swapped by accident:
type Cents = number & { readonly __brand: 'Cents' }
type Dollars = number & { readonly __brand: 'Dollars' }
const toDollars = (c: Cents): Dollars => (c / 100) as Dollars
Read across the tabs: C++ is the only one where the class adapter (private multiple inheritance) is genuinely idiomatic — and the STL ships two of them, std::stack and std::queue. Go's version isn't a stylistic choice at all: the language forbids adding methods to another package's type, so wrapping is the only move available. Python and JavaScript need the pattern least, because matching names are enough there — but note that every sample still performs the same unit and error conversions, and no dynamic type system saves you from those.
05
How to implement Adapter
Write down the target interface — the one your code already speaks. If there isn't one yet, extract it from the call sites first; adapting to a concrete class gets you nothing.
Create the adapter class implementing that interface, taking the adaptee as a constructor argument.
Translate every method: name, argument order, and above all units and types — cents versus dollars, seconds versus milliseconds, string status versus enum.
Translate the error model too. Return codes become exceptions, or vendor exceptions become your error type. Half-translated failures are the classic adapter bug.
Wire the adapter at exactly one place — the composition root. Nothing else in the codebase should import the adaptee's package.
Unit-test the conversions directly with a fake adaptee. This class is small, pure and load-bearing, which makes it the cheapest test you will write all week.
06
When to use Adapter — and when not to
Use it when you need to use an existing class whose interface you cannot change: a third-party SDK, a legacy module, a generated client, an API you don't own. It is also the standard way to keep a vendor at arm's length, so that replacing them later is a change to one file rather than a migration project.
Where it goes wrong
The leaky adapter. The adapter's method returns the vendor's own type — a TxnResult, a StripeCharge, a raw JSON blob. Now the vendor is back in your domain model and you have paid for the pattern without getting the isolation you bought it for. If the vendor's type appears in your target interface, you don't have an adapter.
Adapting an interface you invented five minutes ago. If you control both sides, change one of them. An adapter between two of your own classes is a translation layer that exists only to preserve a mismatch you could delete.
Chains of adapters. An adapter around an adapter around a legacy façade means nobody can answer what actually gets sent over the wire. Two hops is a smell; collapse them.
Silent unit conversions. The very thing the adapter is for is also its most dangerous line. cents / 100 in a language with binary floating point will eventually be wrong by a cent. Use decimals, and test the conversion — the adapter is where money quietly disappears.
You want to…
Use
Because
Make an incompatible interface usable, unchanged in behaviour
Bridge is designed in up front; Adapter is retrofitted to something already shipped.
07
Quick check
🧠 Quick check
Your adapter implements PaymentProvider, but its charge() method returns the vendor's TxnResult class because "it already has everything the callers need". What have you actually built?
The point of the pattern is that the adaptee's vocabulary stops at the adapter. The moment TxnResult is the declared return type, every caller imports the vendor's package, reads the vendor's field names, and branches on the vendor's status strings — so switching processors is once again a change to forty files, which is the exact cost you introduced this class to avoid. Return your own Receipt. It is more code today and one file's worth of change later.
In the wild
Javajava.util.Arrays.asList(array) — adapts a fixed array to the List interface. InputStreamReader is the other classic: it adapts a byte stream to a character stream.
C++std::stack and std::queue are called container adapters in the standard itself — they wrap a std::deque and expose a different interface over it.
Gohttp.HandlerFunc — a function type with one ServeHTTP method, adapting any plain function to the http.Handler interface.
C#DbDataAdapter in ADO.NET, named after the pattern, and TextWriter.Synchronized() which adapts a writer to a thread-safe one.
Pythonio.TextIOWrapper — adapts a binary buffered stream into a text stream, doing the encoding translation in the middle.
JavaScriptArray.from(arrayLike) — adapts anything iterable or array-like (a NodeList, a Set, arguments) to a real array.
What is the difference between the Adapter and Facade patterns?
Adapter makes one class fit an interface that already exists, because your code has to call it and the shapes don't line up. Facade invents a brand-new, simpler interface in front of many classes, because the subsystem is awkward to use even though nothing is incompatible. Two quick tests: an adapter usually wraps one object and is forced on you by a mismatch; a facade usually wraps several and is a convenience you chose. If you could delete the wrapper and the code would still compile — just be uglier — it's a facade.
What is the difference between Adapter and Decorator?
The interface. A Decorator implements the same interface as the object it wraps, which is what lets you stack five of them in any order. An Adapter implements a different interface from its adaptee — that's the entire point — so adapters don't stack. Same wrap-and-delegate shape, opposite relationship to the interface.
What is the difference between a class adapter and an object adapter?
A class adapter inherits from the adaptee and implements the target; an object adapter holds the adaptee as a field. Object adapters win almost everywhere: they work when the adaptee is final or sealed, they can adapt subclasses of the adaptee, they can swap the adaptee at runtime, and they don't inherit the vendor's entire public surface into your class. C++ is the notable exception — private multiple inheritance makes class adapters natural there, and the STL uses them.
Do I need the Adapter pattern in Python or JavaScript?
Less often, and it's worth being honest about why. Duck typing means that if the vendor's method happens to be called charge and takes the same arguments, it already satisfies your "interface" and no adapter is needed. What duck typing does not fix is different argument order, different units, different return shapes and different error conventions — and those are the mismatches that actually cost you. When the difference is only a name, use a small wrapper or a partial; when it's semantics, write the adapter.
Is the Adapter pattern a code smell?
Not at a boundary you don't control — there it is the opposite, and skipping it is the smell. It becomes a smell when both sides are yours (fix one of them instead), when adapters are chained several deep, or when the adapter has grown business logic. A healthy adapter is boring, small, and the only file in the repository that names the vendor.
Finish the handbook, earn the certificate
All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.