Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Adapter

Making an incompatible interface fit — and the one file that should know the vendor's vocabulary.


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:

InterfaceWraps
Adapterchanges it to one you specifyusually one object
Facadeinvents a simpler onea whole subsystem
Decoratorkeeps it identicalone 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.