Adapter
Payments went through one gateway, and its SDK was called directly from 40 files. Then
finance negotiated a better rate with a second gateway, worth about 11 lakh rupees a
year. The new SDK wanted amounts in paise, not rupees. Its method was createCharge, not
pay. It returned { status: 'succeeded' } where the old one returned true.
The estimate to switch was three weeks and 40 files. The saving sat unused for two quarters, waiting for someone to have three free weeks.
Your code calls gateway.pay(amountInRupees) and expects true or false. A new vendor SDK offers createCharge(amountInPaise, currency) and returns an object with a status string. You cannot edit the SDK. How do you use it without touching the 40 callers?
The idea
Your charger has an Indian plug. The hotel wall in London has a British socket. You do not rewire the charger and you do not rebuild the wall. You put a small piece of plastic between them that has the right shape on each side. It adds no electricity of its own. It only makes two things that were never designed for each other fit.
The mismatch
// What every part of our code expects.
interface PaymentGateway {
pay(amountInRupees: number): boolean
}
// The vendor's SDK. We cannot edit it. Different name, unit and result.
type ChargeResult = { status: 'succeeded' | 'failed'; charged: string }
class QuickPaySdk {
createCharge(amountInPaise: number, currency: string): ChargeResult {
const status = amountInPaise > 0 ? 'succeeded' : 'failed'
return { status, charged: `${amountInPaise} ${currency}` }
}
}
function checkout(gateway: PaymentGateway, amount: number): string {
return gateway.pay(amount) ? `Paid ${amount}` : 'Payment failed'
}
checkout(new QuickPaySdk(), 240)
// Error: Argument of type 'QuickPaySdk' is not assignable to
// parameter of type 'PaymentGateway'.checkout wants something with a pay method. QuickPaySdk does not have one, so the
compiler refuses. Three things differ: the method name, the unit of money, and the shape
of the answer.
The adapter
class QuickPayAdapter implements PaymentGateway {
constructor(private readonly sdk: QuickPaySdk) {}
pay(amountInRupees: number): boolean {
const result = this.sdk.createCharge(amountInRupees * 100, 'INR')
return result.status === 'succeeded'
}
}
const gateway = new QuickPayAdapter(new QuickPaySdk())
console.log(checkout(gateway, 240)) // Paid 240
console.log(new QuickPaySdk().createCharge(240 * 100, 'INR').charged)
// > 24000 INREight lines. The adapter implements the interface your code expects, holds the object
it is adapting, and translates in both directions: rupees to paise on the way in, a
status string to a boolean on the way out.
checkout was not edited. Neither were the other 39 callers. The three week migration
is one new class, and one line where the gateway is created.
Why not edit the callers instead
Three reasons, and they are the reasons adapters are everywhere in real backends.
You will switch again. With the adapter in place, a third gateway is a third small class. Without it, it is another 40 files.
The vendor’s vocabulary stays out of your code. Paise, createCharge and status
strings are their words. If they leak into checkout, refunds and reports, a version bump
of their SDK becomes a change to your business logic.
You can run both. Send 10% of traffic through the new gateway by choosing which adapter to create. You cannot do that if the SDK calls are spread across the codebase.
This is the Dependency inversion page from the other side. There, you owned both ends and chose to put an interface between them. Here, you do not own one end, so the adapter is what makes that end fit your interface.
Adapting more than method names
Real adapters also translate errors and data shapes, because those leak the same way.
interface PaymentGateway {
pay(amountInRupees: number): boolean
}
class PaymentDeclined extends Error {}
class OldBankSdk {
debit(rupees: number): void {
if (rupees > 100000) throw new Error('ERR_4012_LIMIT')
}
}
class OldBankAdapter implements PaymentGateway {
constructor(private readonly sdk: OldBankSdk) {}
pay(amountInRupees: number): boolean {
try {
this.sdk.debit(amountInRupees)
return true
} catch {
throw new PaymentDeclined('The bank declined this payment')
}
}
}
const gateway: PaymentGateway = new OldBankAdapter(new OldBankSdk())
console.log(gateway.pay(240)) // true
try {
gateway.pay(250000)
} catch (error) {
console.log(error instanceof PaymentDeclined) // true
}The caller sees PaymentDeclined, an error from your own code, and never learns that
some bank calls it ERR_4012_LIMIT.
The adapter is the wrong home for “orders above 50,000 need a second approval”. It is
tempting, because the adapter is right there on the payment path. But then the rule
applies to one gateway and not the other, and nobody looks for a business rule in a
file called QuickPayAdapter. An adapter translates. If it starts making decisions,
move them out.
All three wrap something, so this comparison is asked constantly. Adapter changes the interface to one the caller already expects. Decorator keeps the same interface and adds behaviour. Facade invents a new, simpler interface in front of several classes. One line each, and you have answered it.
Which wrapper is it?
class QuickPayAdapter implements PaymentGatewayconstructor(private readonly sdk: QuickPaySdk) {}type ChargeResult = { status: 'succeeded' | 'failed'; charged: string }result.status === 'succeeded'class PaymentDeclined extends Error {}catch { ... }Checkpoint
1. What are the two relationships an adapter class has?
2. After introducing QuickPayAdapter, how many of the 40 calling files need to change?
3. Where should the rule "payments above 50,000 rupees need manager approval" live?
An adapter lets two interfaces that do not match work together when I cannot change one of them, typically a vendor SDK. I define the interface my own code wants, write a small class that implements it and holds the SDK object, and each method translates the call: names, units, return shapes, and errors as well. My callers never see the vendor's vocabulary, so switching vendors or running two side by side is a new adapter and one line at startup, not an edit to every caller. The adapter only translates. It holds no business rules. Compared with its neighbours: adapter changes the interface, decorator keeps the interface and adds behaviour, and facade puts a simpler interface in front of several classes.
