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 answer

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

The picture to keep
The travel plug

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.

In one line: Wrap the thing you cannot change so it looks like the interface your code already expects.

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 INR

Eight 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.

Figure 1. The adapter looks like a PaymentGateway from the left and talks to the vendor SDK on the right. Neither side knows the other exists.

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.

Business logic in the adapter

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.

Adapter, Decorator or Facade?

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?

Adapter, or something else?
A class that makes an XML based tax API look like your JSON based TaxService interface.
A class with the same interface as PriceService that adds caching around the real one.
One placeOrder() method that calls inventory, payments, kitchen and riders in order.
A function that wraps a callback style library function so it returns a promise.
A wrapper around console.log that your code calls as logger.info().
TypeScript you just picked up
class QuickPayAdapter implements PaymentGateway
The adapter promises the interface the rest of the code expects.
constructor(private readonly sdk: QuickPaySdk) {}
It holds the object it adapts. Composition, not inheritance.
type ChargeResult = { status: 'succeeded' | 'failed'; charged: string }
A named object type with a literal union inside it.
result.status === 'succeeded'
Comparing against a literal. A typo here is a compile error because status is a union.
class PaymentDeclined extends Error {}
A custom error type, so callers can tell it apart with instanceof.
catch { ... }
A catch with no variable, for when you do not need the original error.

Checkpoint

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?

Say this in 60 seconds

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.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.