Your application wants to charge a card. The payment provider’s SDK wants an
integer number of cents, a lowercase currency code, and a source token — and
it reports failure by returning a status string rather than throwing.
Neither side is wrong. They just do not fit.
What people write first
Speak the vendor’s language at every call site.
// Our code, speaking the vendor’s language.
const res = await stripe.charges.create({
amount: Math.round(total * 100), // they want cents
currency: currency.toLowerCase(),
source: token,
});
if (res.status !== "succeeded") throw new Error(res.failure_message);
return res.id; Every call site learns the vendor's vocabulary — and the day you switch providers, every one of them changes.
It works, and it is fine for one call. The trouble starts at the fifth, because
by then the vendor’s model has spread: Math.round(total * 100) appears in five
files, and so does the knowledge that failure is a string comparison.
Then the vendor deprecates charges in favour of payment_intents, or you add
a second provider for another country, and there are five places to change with
no compiler help.
The pattern
Write the interface you wanted, then one class that translates.
// What our domain says.interface PaymentGateway { charge(amount: Money, token: string): Promise<Receipt>;}
// What the vendor says, translated once.class StripeGateway implements PaymentGateway { constructor(private readonly stripe: Stripe) {}
async charge(amount: Money, token: string): Promise<Receipt> { const res = await this.stripe.charges.create({ amount: amount.minorUnits, currency: amount.currency.toLowerCase(), source: token, });
// Their failure convention becomes ours. if (res.status !== 'succeeded') throw new PaymentFailed(res.failure_message); return {id: res.id, amount}; }}Two shapes of the same idea
Object adapter — hold the adaptee, forward to it. That is the version above, and it is what you should write. It works with any instance, including one you did not construct, and it can adapt several objects at once.
Class adapter — inherit from the adaptee and implement the target interface. Needs multiple inheritance, so it does not exist in TypeScript, Java or C#, and it couples you to a concrete class. Mentioned only because the catalogue lists both.
The interface must come from your side
This is the part people get wrong, and it undoes the whole benefit.
interface PaymentGateway { createCharge(params: {amount: number; currency: string; source: string}): Promise<ChargeResult>;}
interface PaymentGateway { charge(amount: Money, token: string): Promise<Receipt>;}The first “adapter” has the vendor’s shape with your names on it. Swap providers and it does not fit, because the interface was derived from the thing it was supposed to insulate you from.
Design the interface from what your callers want to say. If you have never used a second provider, imagine one — and if you cannot, that is a hint that this abstraction may not be earning its place yet.
Where it already is
Array.from(nodeList) adapts an array-like into an array. An ORM adapts rows to
objects. A logging facade like SLF4J is an adapter over whichever backend is on
the classpath. Every “driver” is an adapter, and the word driver is usually a
better name for it.
Adapter, Facade, Decorator
All three wrap. The difference is what happens to the interface:
| Interface | Wraps | |
|---|---|---|
| Adapter | changes it to one you specify | usually one object |
| Facade | invents a simpler one | a whole subsystem |
| Decorator | keeps it identical | one object, stackably |
Adapter is about incompatibility. Facade is about complexity. Decorator is about addition. If you can say which of those three words describes your problem, you have chosen the pattern.