Wrap a class you can't change so that it looks like the interface your code expects. It is the pattern for every vendor SDK you will ever call.
Your shop app takes card payments. The first provider's SDK counts in paise, answers with a status string and wants an idempotency key. The second provider's SDK counts in whole rupees, answers with an integer code and calls its retry key a "reference". Both can charge a card and refund it. Your checkout wants to say "charge Asha 499 rupees" and get back a payment reference or a decline, and it does not want to learn what a paisa is.
That gap is what the Adapter pattern closes. You can't edit the vendor's code, because it is their SDK, and you shouldn't bend your own code to match it. So you write a small class that implements your interface and, inside, calls their methods and translates. You have already met one: SdkCharger in the lesson on interface segregation, dependency inversion and injection was an adapter, the class that turned the checkout's "charge this token" into the vendor's paise and status string. This lesson names the pattern, builds it for two vendors, and settles the question every textbook raises: object adapter or class adapter, and which one each language lets you write. Three case studies lean on it: movie booking has the SeatLockProvider seam, the ATM has a Bank, and food delivery has a PaymentGateway with the same two methods this lesson builds.
A travel plug adapter changes neither your charger nor the hotel's wall: it sits between them and changes the shape. A payment adapter does that for an SDK.
The first version: the checkout talks to both SDKs#
The first provider is wired in and it works. Then the business signs a second one for another region, and the checkout learns both:
# first_version.py: the checkout talks to both vendor SDKs, so it knows paise, rupees, status strings and codes
class AlphaCharge:
def __init__(self, id: str, status: str) -> None:
self.id, self.status = id, status
class AlphaSdk: # vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
def __init__(self, balances) -> None:
self.balances, self.charges, self.last_paise = dict(balances), {}, 0
def create_charge(self, token: str, paise: int, idempotency_key: str) -> AlphaCharge:
self.last_paise = paise
if idempotency_key not in self.charges:
ok = self.balances.get(token, 0) >= paise
if ok:
self.balances[token] -= paise
self.charges[idempotency_key] = AlphaCharge(f"al_{len(self.charges) + 1}" if ok else "", "succeeded" if ok else "declined")
return self.charges[idempotency_key]
def refund_charge(self, charge_id: str, paise: int) -> str:
return "refunded"
class BetaReceipt:
def __init__(self, code: int, number: str) -> None:
self.code, self.number = code, number
class BetaSdk: # vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
def __init__(self, balances) -> None:
self.balances, self.receipts, self.last_rupees = dict(balances), {}, 0
def pay(self, card: str, rupees: int, reference: str) -> BetaReceipt:
self.last_rupees = rupees
if reference not in self.receipts:
ok = self.balances.get(card, 0) >= rupees
if ok:
self.balances[card] -= rupees
self.receipts[reference] = BetaReceipt(0 if ok else 51, f"bt_{len(self.receipts) + 1}" if ok else "")
return self.receipts[reference]
def reverse(self, receipt: str, rupees: int) -> int:
return 0
class Checkout:
def __init__(self, provider: str, alpha: AlphaSdk, beta: BetaSdk) -> None:
self.provider, self.alpha, self.beta, self.refs = provider, alpha, beta, {}
def pay(self, order_id: str, customer: str, amount: int) -> str:
if self.provider == "alpha":
charge = self.alpha.create_charge("tok_" + customer, amount * 100, order_id) # this vendor wants paise
if charge.status != "succeeded":
return f"declined {amount}"
ref = charge.id
elif self.provider == "beta":
receipt = self.beta.pay("card_" + customer, amount, order_id) # this vendor wants rupees
if receipt.code != 0:
return f"declined {amount}"
ref = receipt.number
else:
raise ValueError(self.provider)
self.refs[order_id] = ref
return f"paid {amount}, ref {ref}"
def refund(self, order_id: str, amount: int) -> str:
ref = self.refs[order_id]
if self.provider == "alpha":
self.alpha.refund_charge(ref, amount * 100)
elif self.provider == "beta":
self.beta.reverse(ref, amount)
else:
raise ValueError(self.provider)
return f"refunded {amount} of {order_id}"
alpha = AlphaSdk({"tok_asha": 100000, "tok_ravi": 30000})
beta = BetaSdk({"card_asha": 1000, "card_ravi": 300})
for provider in ("alpha", "beta"):
checkout = Checkout(provider, alpha, beta)
print(f"{provider}: {checkout.pay('o-1', 'asha', 499)}")
print(f"{provider}: retry with the same key: {checkout.pay('o-1', 'asha', 499)}")
print(f"{provider}: {checkout.pay('o-2', 'ravi', 499)}")
print(f"{provider}: {checkout.refund('o-1', 100)}")
print(f"units the SDKs saw: alpha {alpha.last_paise} paise, beta {beta.last_rupees} rupees")// FirstVersion.java: the checkout talks to both vendor SDKs, so it knows paise, rupees, status strings and codes
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class FirstVersion {
record AlphaCharge(String id, String status) {}
static final class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
final Map<String, Integer> balances;
final Map<String, AlphaCharge> charges = new HashMap<>();
int lastPaise = 0;
AlphaSdk(Map<String, Integer> balances) { this.balances = new HashMap<>(balances); }
AlphaCharge createCharge(String token, int paise, String idempotencyKey) {
lastPaise = paise;
if (!charges.containsKey(idempotencyKey)) {
boolean ok = balances.getOrDefault(token, 0) >= paise;
if (ok) balances.merge(token, -paise, Integer::sum);
charges.put(idempotencyKey, new AlphaCharge(ok ? "al_" + (charges.size() + 1) : "", ok ? "succeeded" : "declined"));
}
return charges.get(idempotencyKey);
}
String refundCharge(String chargeId, int paise) { return "refunded"; }
}
record BetaReceipt(int code, String number) {}
static final class BetaSdk { // vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
final Map<String, Integer> balances;
final Map<String, BetaReceipt> receipts = new HashMap<>();
int lastRupees = 0;
BetaSdk(Map<String, Integer> balances) { this.balances = new HashMap<>(balances); }
BetaReceipt pay(String card, int rupees, String reference) {
lastRupees = rupees;
if (!receipts.containsKey(reference)) {
boolean ok = balances.getOrDefault(card, 0) >= rupees;
if (ok) balances.merge(card, -rupees, Integer::sum);
receipts.put(reference, new BetaReceipt(ok ? 0 : 51, ok ? "bt_" + (receipts.size() + 1) : ""));
}
return receipts.get(reference);
}
int reverse(String receipt, int rupees) { return 0; }
}
static final class Checkout {
private final String provider;
private final AlphaSdk alpha;
private final BetaSdk beta;
private final Map<String, String> refs = new HashMap<>();
Checkout(String provider, AlphaSdk alpha, BetaSdk beta) {
this.provider = provider;
this.alpha = alpha;
this.beta = beta;
}
String pay(String orderId, String customer, int amount) {
String ref;
if (provider.equals("alpha")) {
AlphaCharge charge = alpha.createCharge("tok_" + customer, amount * 100, orderId); // this vendor wants paise
if (!charge.status().equals("succeeded")) return "declined " + amount;
ref = charge.id();
} else if (provider.equals("beta")) {
BetaReceipt receipt = beta.pay("card_" + customer, amount, orderId); // this vendor wants rupees
if (receipt.code() != 0) return "declined " + amount;
ref = receipt.number();
} else {
throw new IllegalArgumentException(provider);
}
refs.put(orderId, ref);
return "paid " + amount + ", ref " + ref;
}
String refund(String orderId, int amount) {
String ref = refs.get(orderId);
if (provider.equals("alpha")) alpha.refundCharge(ref, amount * 100);
else if (provider.equals("beta")) beta.reverse(ref, amount);
else throw new IllegalArgumentException(provider);
return "refunded " + amount + " of " + orderId;
}
}
public static void main(String[] args) {
AlphaSdk alpha = new AlphaSdk(Map.of("tok_asha", 100000, "tok_ravi", 30000));
BetaSdk beta = new BetaSdk(Map.of("card_asha", 1000, "card_ravi", 300));
for (String provider : List.of("alpha", "beta")) {
Checkout checkout = new Checkout(provider, alpha, beta);
System.out.println(provider + ": " + checkout.pay("o-1", "asha", 499));
System.out.println(provider + ": retry with the same key: " + checkout.pay("o-1", "asha", 499));
System.out.println(provider + ": " + checkout.pay("o-2", "ravi", 499));
System.out.println(provider + ": " + checkout.refund("o-1", 100));
}
System.out.println("units the SDKs saw: alpha " + alpha.lastPaise + " paise, beta " + beta.lastRupees + " rupees");
}
}// first_version.cpp: the checkout talks to both vendor SDKs, so it knows paise, rupees, status strings and codes
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
struct AlphaCharge {
std::string id;
std::string status;
};
class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
std::map<std::string, int> balances_;
std::map<std::string, AlphaCharge> charges_;
public:
int last_paise = 0;
explicit AlphaSdk(std::map<std::string, int> balances) : balances_(std::move(balances)) {}
AlphaCharge create_charge(const std::string& token, int paise, const std::string& idempotency_key) {
last_paise = paise;
if (charges_.count(idempotency_key) == 0) {
bool ok = balances_[token] >= paise;
if (ok) balances_[token] -= paise;
charges_[idempotency_key] = AlphaCharge{ok ? "al_" + std::to_string(charges_.size() + 1) : "", ok ? "succeeded" : "declined"};
}
return charges_[idempotency_key];
}
std::string refund_charge(const std::string& /*charge_id*/, int /*paise*/) { return "refunded"; }
};
struct BetaReceipt {
int code;
std::string number;
};
class BetaSdk { // vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
std::map<std::string, int> balances_;
std::map<std::string, BetaReceipt> receipts_;
public:
int last_rupees = 0;
explicit BetaSdk(std::map<std::string, int> balances) : balances_(std::move(balances)) {}
BetaReceipt pay(const std::string& card, int rupees, const std::string& reference) {
last_rupees = rupees;
if (receipts_.count(reference) == 0) {
bool ok = balances_[card] >= rupees;
if (ok) balances_[card] -= rupees;
receipts_[reference] = BetaReceipt{ok ? 0 : 51, ok ? "bt_" + std::to_string(receipts_.size() + 1) : ""};
}
return receipts_[reference];
}
int reverse(const std::string& /*receipt*/, int /*rupees*/) { return 0; }
};
class Checkout {
std::string provider_;
AlphaSdk& alpha_;
BetaSdk& beta_;
std::map<std::string, std::string> refs_;
public:
Checkout(std::string provider, AlphaSdk& alpha, BetaSdk& beta) : provider_(std::move(provider)), alpha_(alpha), beta_(beta) {}
std::string pay(const std::string& order_id, const std::string& customer, int amount) {
std::string ref;
if (provider_ == "alpha") {
AlphaCharge charge = alpha_.create_charge("tok_" + customer, amount * 100, order_id); // this vendor wants paise
if (charge.status != "succeeded") return "declined " + std::to_string(amount);
ref = charge.id;
} else if (provider_ == "beta") {
BetaReceipt receipt = beta_.pay("card_" + customer, amount, order_id); // this vendor wants rupees
if (receipt.code != 0) return "declined " + std::to_string(amount);
ref = receipt.number;
} else {
throw std::invalid_argument(provider_);
}
refs_[order_id] = ref;
return "paid " + std::to_string(amount) + ", ref " + ref;
}
std::string refund(const std::string& order_id, int amount) {
const std::string& ref = refs_[order_id];
if (provider_ == "alpha") alpha_.refund_charge(ref, amount * 100);
else if (provider_ == "beta") beta_.reverse(ref, amount);
else throw std::invalid_argument(provider_);
return "refunded " + std::to_string(amount) + " of " + order_id;
}
};
int main() {
AlphaSdk alpha({{"tok_asha", 100000}, {"tok_ravi", 30000}});
BetaSdk beta({{"card_asha", 1000}, {"card_ravi", 300}});
for (const std::string provider : {"alpha", "beta"}) {
Checkout checkout(provider, alpha, beta);
std::cout << provider << ": " << checkout.pay("o-1", "asha", 499) << "\n";
std::cout << provider << ": retry with the same key: " << checkout.pay("o-1", "asha", 499) << "\n";
std::cout << provider << ": " << checkout.pay("o-2", "ravi", 499) << "\n";
std::cout << provider << ": " << checkout.refund("o-1", 100) << "\n";
}
std::cout << "units the SDKs saw: alpha " << alpha.last_paise << " paise, beta " << beta.last_rupees << " rupees\n";
}// firstVersion.js: the checkout talks to both vendor SDKs, so it knows paise, rupees, status strings and codes
class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
constructor(balances) {
this.balances = { ...balances };
this.charges = new Map();
this.lastPaise = 0;
}
createCharge(token, paise, idempotencyKey) {
this.lastPaise = paise;
if (!this.charges.has(idempotencyKey)) {
const ok = (this.balances[token] ?? 0) >= paise;
if (ok) this.balances[token] -= paise;
this.charges.set(idempotencyKey, { id: ok ? `al_${this.charges.size + 1}` : "", status: ok ? "succeeded" : "declined" });
}
return this.charges.get(idempotencyKey);
}
refundCharge(chargeId, paise) { return "refunded"; }
}
class BetaSdk { // vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
constructor(balances) {
this.balances = { ...balances };
this.receipts = new Map();
this.lastRupees = 0;
}
pay(card, rupees, reference) {
this.lastRupees = rupees;
if (!this.receipts.has(reference)) {
const ok = (this.balances[card] ?? 0) >= rupees;
if (ok) this.balances[card] -= rupees;
this.receipts.set(reference, { code: ok ? 0 : 51, number: ok ? `bt_${this.receipts.size + 1}` : "" });
}
return this.receipts.get(reference);
}
reverse(receipt, rupees) { return 0; }
}
class Checkout {
constructor(provider, alpha, beta) {
this.provider = provider;
this.alpha = alpha;
this.beta = beta;
this.refs = new Map();
}
pay(orderId, customer, amount) {
let ref;
if (this.provider === "alpha") {
const charge = this.alpha.createCharge("tok_" + customer, amount * 100, orderId); // this vendor wants paise
if (charge.status !== "succeeded") return `declined ${amount}`;
ref = charge.id;
} else if (this.provider === "beta") {
const receipt = this.beta.pay("card_" + customer, amount, orderId); // this vendor wants rupees
if (receipt.code !== 0) return `declined ${amount}`;
ref = receipt.number;
} else {
throw new Error(this.provider);
}
this.refs.set(orderId, ref);
return `paid ${amount}, ref ${ref}`;
}
refund(orderId, amount) {
const ref = this.refs.get(orderId);
if (this.provider === "alpha") this.alpha.refundCharge(ref, amount * 100);
else if (this.provider === "beta") this.beta.reverse(ref, amount);
else throw new Error(this.provider);
return `refunded ${amount} of ${orderId}`;
}
}
const alpha = new AlphaSdk({ tok_asha: 100000, tok_ravi: 30000 });
const beta = new BetaSdk({ card_asha: 1000, card_ravi: 300 });
for (const provider of ["alpha", "beta"]) {
const checkout = new Checkout(provider, alpha, beta);
console.log(`${provider}: ${checkout.pay("o-1", "asha", 499)}`);
console.log(`${provider}: retry with the same key: ${checkout.pay("o-1", "asha", 499)}`);
console.log(`${provider}: ${checkout.pay("o-2", "ravi", 499)}`);
console.log(`${provider}: ${checkout.refund("o-1", 100)}`);
}
console.log(`units the SDKs saw: alpha ${alpha.lastPaise} paise, beta ${beta.lastRupees} rupees`);alpha: paid 499, ref al_1
alpha: retry with the same key: paid 499, ref al_1
alpha: declined 499
alpha: refunded 100 of o-1
beta: paid 499, ref bt_1
beta: retry with the same key: paid 499, ref bt_1
beta: declined 499
beta: refunded 100 of o-1
units the SDKs saw: alpha 49900 paise, beta 499 rupeesThe numbers: the customer pays 499 rupees. Alpha counts in paise, so the checkout sends 499 × 100 = 49,900, while Beta counts in rupees and gets 499. The last line shows it: each SDK saw its own unit. "Declined" is the status string declined from Alpha and the code 51 from Beta (Ravi has 300 rupees, which is 30,000 paise, and 499 rupees is more than that in either unit), and the checkout turns both into the same words. The retry with the same order id gets the same reference back (al_1, bt_1) because both vendors deduplicate on the key: Alpha calls it the idempotency key and Beta calls it the reference, and the checkout passes the order id to both.
It runs, and two providers fit on one screen. The trouble is the next provider, and the next method:
- The checkout knows the vendors. It converts to paise, reads a status string and reads a code. Those are the vendors' habits, and they now sit inside your business logic.
- A third provider edits two methods.
payandrefundeach branch on the provider: 2 methods with 2 branches each is 4 branches today, and every method you add later (a payment status, a payout) needs one more branch per provider. - A unit slip is quiet.
amount * 100appears twice, once in each Alpha branch. Forget it once and a 499-rupee order is charged 499 paise, which is 4.99 rupees. - A test has to fake a vendor. To test
payyou build an SDK stand-in that copies the vendor's paise and codes, instead of a two-method interface of your own.
Before the refactor: where does the vendor stop?
The business signs a third provider that counts in cents, reports errors as a text message and calls the retry key nonce. Which parts of the first checkout change, and which part of the job stays the same?
One strong answer
pay and refund change, one new branch each, and so does every method you add later. The job doesn't change. Every branch does the same two things: charge this customer this many rupees and give back a reference or a decline, and refund a payment. The vendor's habits are the part that varies. So they belong in one class per vendor, behind an interface the checkout owns, and the checkout never sees the difference.
The pattern#
The book that named it (Design Patterns, 1994, by Gamma, Helm, Johnson and Vlissides) states the intent of Adapter as: "Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces." The book also lists a second name for it, Wrapper.
There are four roles, and they map onto the payment code one to one:
| Role | In the payment code | Its one job |
|---|---|---|
| Target | PaymentGateway |
The interface the client expects: rupees in, one reference out, one exception for "declined" |
| Client | Checkout |
Uses the target and knows nothing else |
| Adaptee | AlphaSdk, BetaSdk |
The class you can't change, with the wrong interface |
| Adapter | AlphaAdapter, BetaAdapter |
Implements the target by calling the adaptee and translating |
%% title: Adapter: one target, one adapter per vendor SDK
classDiagram
class PaymentGateway {
<<interface>>
+charge(customer, amount, key) str
+refund(payment_ref, amount)
}
class Checkout {
+pay(order_id, customer, amount) str
+refund(order_id, amount) str
}
class AlphaAdapter
class BetaAdapter
class FakeGateway
class AlphaSdk
class BetaSdk
Checkout --> PaymentGateway : charges through
PaymentGateway <|.. AlphaAdapter
PaymentGateway <|.. BetaAdapter
PaymentGateway <|.. FakeGateway
AlphaAdapter --> AlphaSdk : wraps
BetaAdapter --> BetaSdk : wrapsRead the arrows. Checkout points at the interface and nothing else, so it can't tell which vendor is behind it. Each adapter points up at the interface it implements and down at the one SDK it wraps: the translation happens on that second arrow, and the SDK knows nothing about it. And the interface belongs to the checkout's side. If charge returned Alpha's result type, PaymentGateway would be Alpha's interface with a new name, and the checkout would still depend on the vendor. This is the arrow flip from the dependency-inversion lesson, drawn for a real SDK.
One charge, arrow by arrow: the checkout asks in rupees, the adapter converts to paise for the SDK, and turns the vendor's answer back into a reference.
The code: two adapters#
Here is the refactor in all four languages. The SDK stand-ins are exactly the ones from the first version. The checkout loses every vendor name, each vendor gets an adapter, and a fake joins in as a third implementation:
# adapters.py: two object adapters and a fake behind one PaymentGateway; the checkout names no vendor
from abc import ABC, abstractmethod
class AlphaCharge:
def __init__(self, id: str, status: str) -> None:
self.id, self.status = id, status
class AlphaSdk: # vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
def __init__(self, balances) -> None:
self.balances, self.charges, self.last_paise = dict(balances), {}, 0
def create_charge(self, token: str, paise: int, idempotency_key: str) -> AlphaCharge:
self.last_paise = paise
if idempotency_key not in self.charges:
ok = self.balances.get(token, 0) >= paise
if ok:
self.balances[token] -= paise
self.charges[idempotency_key] = AlphaCharge(f"al_{len(self.charges) + 1}" if ok else "", "succeeded" if ok else "declined")
return self.charges[idempotency_key]
def refund_charge(self, charge_id: str, paise: int) -> str:
return "refunded"
class BetaReceipt:
def __init__(self, code: int, number: str) -> None:
self.code, self.number = code, number
class BetaSdk: # vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
def __init__(self, balances) -> None:
self.balances, self.receipts, self.last_rupees = dict(balances), {}, 0
def pay(self, card: str, rupees: int, reference: str) -> BetaReceipt:
self.last_rupees = rupees
if reference not in self.receipts:
ok = self.balances.get(card, 0) >= rupees
if ok:
self.balances[card] -= rupees
self.receipts[reference] = BetaReceipt(0 if ok else 51, f"bt_{len(self.receipts) + 1}" if ok else "")
return self.receipts[reference]
def reverse(self, receipt: str, rupees: int) -> int:
return 0
class PaymentDeclined(Exception):
pass
class PaymentGateway(ABC): # the target: what the checkout needs, in the checkout's own terms (rupees, one ref, one exception)
@abstractmethod
def charge(self, customer: str, amount: int, idempotency_key: str) -> str: ...
@abstractmethod
def refund(self, payment_ref: str, amount: int) -> None: ...
class AlphaAdapter(PaymentGateway): # an object adapter: it holds the SDK and translates
def __init__(self, sdk: AlphaSdk) -> None:
self.sdk = sdk
def charge(self, customer: str, amount: int, idempotency_key: str) -> str:
result = self.sdk.create_charge("tok_" + customer, amount * 100, idempotency_key) # rupees become paise
if result.status != "succeeded":
raise PaymentDeclined(result.status) # the vendor's status string becomes our exception
return result.id
def refund(self, payment_ref: str, amount: int) -> None:
self.sdk.refund_charge(payment_ref, amount * 100)
class BetaAdapter(PaymentGateway):
def __init__(self, sdk: BetaSdk) -> None:
self.sdk = sdk
def charge(self, customer: str, amount: int, idempotency_key: str) -> str:
receipt = self.sdk.pay("card_" + customer, amount, idempotency_key) # our key is the vendor's reference
if receipt.code != 0:
raise PaymentDeclined(f"code {receipt.code}") # the vendor's integer code becomes our exception
return receipt.number
def refund(self, payment_ref: str, amount: int) -> None:
self.sdk.reverse(payment_ref, amount)
class FakeGateway(PaymentGateway): # a test double is a third implementation of the same target
def __init__(self, broke) -> None:
self.broke, self.refs = set(broke), {}
def charge(self, customer: str, amount: int, idempotency_key: str) -> str:
if customer in self.broke:
raise PaymentDeclined("no funds")
return self.refs.setdefault(idempotency_key, f"fake_{len(self.refs) + 1}")
def refund(self, payment_ref: str, amount: int) -> None:
pass
class Checkout: # the client: it names no vendor, no unit and no status code
def __init__(self, gateway: PaymentGateway) -> None:
self.gateway, self.refs = gateway, {}
def pay(self, order_id: str, customer: str, amount: int) -> str:
try:
ref = self.gateway.charge(customer, amount, order_id)
except PaymentDeclined:
return f"declined {amount}"
self.refs[order_id] = ref
return f"paid {amount}, ref {ref}"
def refund(self, order_id: str, amount: int) -> str:
self.gateway.refund(self.refs[order_id], amount)
return f"refunded {amount} of {order_id}"
def run(label: str, checkout: Checkout) -> None: # one script drives every gateway
print(f"{label}: {checkout.pay('o-1', 'asha', 499)}")
print(f"{label}: retry with the same key: {checkout.pay('o-1', 'asha', 499)}")
print(f"{label}: {checkout.pay('o-2', 'ravi', 499)}")
print(f"{label}: {checkout.refund('o-1', 100)}")
alpha = AlphaSdk({"tok_asha": 100000, "tok_ravi": 30000})
beta = BetaSdk({"card_asha": 1000, "card_ravi": 300})
run("alpha", Checkout(AlphaAdapter(alpha)))
run("beta", Checkout(BetaAdapter(beta)))
run("fake", Checkout(FakeGateway({"ravi"})))
print(f"units the SDKs saw: alpha {alpha.last_paise} paise, beta {beta.last_rupees} rupees")// Adapters.java: two object adapters and a fake behind one PaymentGateway; the checkout names no vendor
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
class Adapters {
record AlphaCharge(String id, String status) {}
static final class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
final Map<String, Integer> balances;
final Map<String, AlphaCharge> charges = new HashMap<>();
int lastPaise = 0;
AlphaSdk(Map<String, Integer> balances) { this.balances = new HashMap<>(balances); }
AlphaCharge createCharge(String token, int paise, String idempotencyKey) {
lastPaise = paise;
if (!charges.containsKey(idempotencyKey)) {
boolean ok = balances.getOrDefault(token, 0) >= paise;
if (ok) balances.merge(token, -paise, Integer::sum);
charges.put(idempotencyKey, new AlphaCharge(ok ? "al_" + (charges.size() + 1) : "", ok ? "succeeded" : "declined"));
}
return charges.get(idempotencyKey);
}
String refundCharge(String chargeId, int paise) { return "refunded"; }
}
record BetaReceipt(int code, String number) {}
static final class BetaSdk { // vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
final Map<String, Integer> balances;
final Map<String, BetaReceipt> receipts = new HashMap<>();
int lastRupees = 0;
BetaSdk(Map<String, Integer> balances) { this.balances = new HashMap<>(balances); }
BetaReceipt pay(String card, int rupees, String reference) {
lastRupees = rupees;
if (!receipts.containsKey(reference)) {
boolean ok = balances.getOrDefault(card, 0) >= rupees;
if (ok) balances.merge(card, -rupees, Integer::sum);
receipts.put(reference, new BetaReceipt(ok ? 0 : 51, ok ? "bt_" + (receipts.size() + 1) : ""));
}
return receipts.get(reference);
}
int reverse(String receipt, int rupees) { return 0; }
}
static final class PaymentDeclined extends RuntimeException {
PaymentDeclined(String why) { super(why); }
}
interface PaymentGateway { // the target: what the checkout needs, in the checkout's own terms (rupees, one ref, one exception)
String charge(String customer, int amount, String idempotencyKey);
void refund(String paymentRef, int amount);
}
static final class AlphaAdapter implements PaymentGateway { // an object adapter: it holds the SDK and translates
private final AlphaSdk sdk;
AlphaAdapter(AlphaSdk sdk) { this.sdk = sdk; }
public String charge(String customer, int amount, String idempotencyKey) {
AlphaCharge result = sdk.createCharge("tok_" + customer, amount * 100, idempotencyKey); // rupees become paise
if (!result.status().equals("succeeded")) throw new PaymentDeclined(result.status()); // the vendor's status string becomes our exception
return result.id();
}
public void refund(String paymentRef, int amount) { sdk.refundCharge(paymentRef, amount * 100); }
}
static final class BetaAdapter implements PaymentGateway {
private final BetaSdk sdk;
BetaAdapter(BetaSdk sdk) { this.sdk = sdk; }
public String charge(String customer, int amount, String idempotencyKey) {
BetaReceipt receipt = sdk.pay("card_" + customer, amount, idempotencyKey); // our key is the vendor's reference
if (receipt.code() != 0) throw new PaymentDeclined("code " + receipt.code()); // the vendor's integer code becomes our exception
return receipt.number();
}
public void refund(String paymentRef, int amount) { sdk.reverse(paymentRef, amount); }
}
static final class FakeGateway implements PaymentGateway { // a test double is a third implementation of the same target
private final Set<String> broke;
private final Map<String, String> refs = new HashMap<>();
FakeGateway(Set<String> broke) { this.broke = broke; }
public String charge(String customer, int amount, String idempotencyKey) {
if (broke.contains(customer)) throw new PaymentDeclined("no funds");
return refs.computeIfAbsent(idempotencyKey, k -> "fake_" + (refs.size() + 1));
}
public void refund(String paymentRef, int amount) {}
}
static final class Checkout { // the client: it names no vendor, no unit and no status code
private final PaymentGateway gateway;
private final Map<String, String> refs = new HashMap<>();
Checkout(PaymentGateway gateway) { this.gateway = gateway; }
String pay(String orderId, String customer, int amount) {
String ref;
try {
ref = gateway.charge(customer, amount, orderId);
} catch (PaymentDeclined e) {
return "declined " + amount;
}
refs.put(orderId, ref);
return "paid " + amount + ", ref " + ref;
}
String refund(String orderId, int amount) {
gateway.refund(refs.get(orderId), amount);
return "refunded " + amount + " of " + orderId;
}
}
static void run(String label, Checkout checkout) { // one script drives every gateway
System.out.println(label + ": " + checkout.pay("o-1", "asha", 499));
System.out.println(label + ": retry with the same key: " + checkout.pay("o-1", "asha", 499));
System.out.println(label + ": " + checkout.pay("o-2", "ravi", 499));
System.out.println(label + ": " + checkout.refund("o-1", 100));
}
public static void main(String[] args) {
AlphaSdk alpha = new AlphaSdk(Map.of("tok_asha", 100000, "tok_ravi", 30000));
BetaSdk beta = new BetaSdk(Map.of("card_asha", 1000, "card_ravi", 300));
run("alpha", new Checkout(new AlphaAdapter(alpha)));
run("beta", new Checkout(new BetaAdapter(beta)));
run("fake", new Checkout(new FakeGateway(Set.of("ravi"))));
System.out.println("units the SDKs saw: alpha " + alpha.lastPaise + " paise, beta " + beta.lastRupees + " rupees");
}
}// adapters.cpp: two object adapters and a fake behind one PaymentGateway; the checkout names no vendor
#include <iostream>
#include <map>
#include <set>
#include <stdexcept>
#include <string>
#include <utility>
struct AlphaCharge {
std::string id;
std::string status;
};
class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
std::map<std::string, int> balances_;
std::map<std::string, AlphaCharge> charges_;
public:
int last_paise = 0;
explicit AlphaSdk(std::map<std::string, int> balances) : balances_(std::move(balances)) {}
AlphaCharge create_charge(const std::string& token, int paise, const std::string& idempotency_key) {
last_paise = paise;
if (charges_.count(idempotency_key) == 0) {
bool ok = balances_[token] >= paise;
if (ok) balances_[token] -= paise;
charges_[idempotency_key] = AlphaCharge{ok ? "al_" + std::to_string(charges_.size() + 1) : "", ok ? "succeeded" : "declined"};
}
return charges_[idempotency_key];
}
std::string refund_charge(const std::string& /*charge_id*/, int /*paise*/) { return "refunded"; }
};
struct BetaReceipt {
int code;
std::string number;
};
class BetaSdk { // vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
std::map<std::string, int> balances_;
std::map<std::string, BetaReceipt> receipts_;
public:
int last_rupees = 0;
explicit BetaSdk(std::map<std::string, int> balances) : balances_(std::move(balances)) {}
BetaReceipt pay(const std::string& card, int rupees, const std::string& reference) {
last_rupees = rupees;
if (receipts_.count(reference) == 0) {
bool ok = balances_[card] >= rupees;
if (ok) balances_[card] -= rupees;
receipts_[reference] = BetaReceipt{ok ? 0 : 51, ok ? "bt_" + std::to_string(receipts_.size() + 1) : ""};
}
return receipts_[reference];
}
int reverse(const std::string& /*receipt*/, int /*rupees*/) { return 0; }
};
class PaymentDeclined : public std::runtime_error {
public:
explicit PaymentDeclined(const std::string& why) : std::runtime_error(why) {}
};
class PaymentGateway { // the target: what the checkout needs, in the checkout's own terms (rupees, one ref, one exception)
public:
virtual ~PaymentGateway() = default;
virtual std::string charge(const std::string& customer, int amount, const std::string& idempotency_key) = 0;
virtual void refund(const std::string& payment_ref, int amount) = 0;
};
class AlphaAdapter : public PaymentGateway { // an object adapter: it holds the SDK and translates
AlphaSdk& sdk_;
public:
explicit AlphaAdapter(AlphaSdk& sdk) : sdk_(sdk) {}
std::string charge(const std::string& customer, int amount, const std::string& idempotency_key) override {
AlphaCharge result = sdk_.create_charge("tok_" + customer, amount * 100, idempotency_key); // rupees become paise
if (result.status != "succeeded") throw PaymentDeclined(result.status); // the vendor's status string becomes our exception
return result.id;
}
void refund(const std::string& payment_ref, int amount) override { sdk_.refund_charge(payment_ref, amount * 100); }
};
class BetaAdapter : public PaymentGateway {
BetaSdk& sdk_;
public:
explicit BetaAdapter(BetaSdk& sdk) : sdk_(sdk) {}
std::string charge(const std::string& customer, int amount, const std::string& idempotency_key) override {
BetaReceipt receipt = sdk_.pay("card_" + customer, amount, idempotency_key); // our key is the vendor's reference
if (receipt.code != 0) throw PaymentDeclined("code " + std::to_string(receipt.code)); // the vendor's integer code becomes our exception
return receipt.number;
}
void refund(const std::string& payment_ref, int amount) override { sdk_.reverse(payment_ref, amount); }
};
class FakeGateway : public PaymentGateway { // a test double is a third implementation of the same target
std::set<std::string> broke_;
std::map<std::string, std::string> refs_;
public:
explicit FakeGateway(std::set<std::string> broke) : broke_(std::move(broke)) {}
std::string charge(const std::string& customer, int /*amount*/, const std::string& idempotency_key) override {
if (broke_.count(customer) > 0) throw PaymentDeclined("no funds");
if (refs_.count(idempotency_key) == 0) refs_[idempotency_key] = "fake_" + std::to_string(refs_.size() + 1);
return refs_[idempotency_key];
}
void refund(const std::string& /*payment_ref*/, int /*amount*/) override {}
};
class Checkout { // the client: it names no vendor, no unit and no status code
PaymentGateway& gateway_;
std::map<std::string, std::string> refs_;
public:
explicit Checkout(PaymentGateway& gateway) : gateway_(gateway) {}
std::string pay(const std::string& order_id, const std::string& customer, int amount) {
std::string ref;
try {
ref = gateway_.charge(customer, amount, order_id);
} catch (const PaymentDeclined&) {
return "declined " + std::to_string(amount);
}
refs_[order_id] = ref;
return "paid " + std::to_string(amount) + ", ref " + ref;
}
std::string refund(const std::string& order_id, int amount) {
gateway_.refund(refs_[order_id], amount);
return "refunded " + std::to_string(amount) + " of " + order_id;
}
};
void run(const std::string& label, Checkout checkout) { // one script drives every gateway
std::cout << label << ": " << checkout.pay("o-1", "asha", 499) << "\n";
std::cout << label << ": retry with the same key: " << checkout.pay("o-1", "asha", 499) << "\n";
std::cout << label << ": " << checkout.pay("o-2", "ravi", 499) << "\n";
std::cout << label << ": " << checkout.refund("o-1", 100) << "\n";
}
int main() {
AlphaSdk alpha({{"tok_asha", 100000}, {"tok_ravi", 30000}});
BetaSdk beta({{"card_asha", 1000}, {"card_ravi", 300}});
AlphaAdapter alpha_adapter(alpha);
BetaAdapter beta_adapter(beta);
FakeGateway fake({"ravi"});
run("alpha", Checkout(alpha_adapter));
run("beta", Checkout(beta_adapter));
run("fake", Checkout(fake));
std::cout << "units the SDKs saw: alpha " << alpha.last_paise << " paise, beta " << beta.last_rupees << " rupees\n";
}// adapters.js: two object adapters and a fake behind one PaymentGateway; the checkout names no vendor
class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
constructor(balances) {
this.balances = { ...balances };
this.charges = new Map();
this.lastPaise = 0;
}
createCharge(token, paise, idempotencyKey) {
this.lastPaise = paise;
if (!this.charges.has(idempotencyKey)) {
const ok = (this.balances[token] ?? 0) >= paise;
if (ok) this.balances[token] -= paise;
this.charges.set(idempotencyKey, { id: ok ? `al_${this.charges.size + 1}` : "", status: ok ? "succeeded" : "declined" });
}
return this.charges.get(idempotencyKey);
}
refundCharge(chargeId, paise) { return "refunded"; }
}
class BetaSdk { // vendor B, as shipped: whole rupees, an integer code (0 paid, 51 no funds), a receipt number
constructor(balances) {
this.balances = { ...balances };
this.receipts = new Map();
this.lastRupees = 0;
}
pay(card, rupees, reference) {
this.lastRupees = rupees;
if (!this.receipts.has(reference)) {
const ok = (this.balances[card] ?? 0) >= rupees;
if (ok) this.balances[card] -= rupees;
this.receipts.set(reference, { code: ok ? 0 : 51, number: ok ? `bt_${this.receipts.size + 1}` : "" });
}
return this.receipts.get(reference);
}
reverse(receipt, rupees) { return 0; }
}
// the target has no interface keyword in JavaScript: any object with charge(customer, amount, key) and refund(ref, amount) fits
class PaymentDeclined extends Error {}
class AlphaAdapter { // an object adapter: it holds the SDK and translates
constructor(sdk) { this.sdk = sdk; }
charge(customer, amount, idempotencyKey) {
const result = this.sdk.createCharge("tok_" + customer, amount * 100, idempotencyKey); // rupees become paise
if (result.status !== "succeeded") throw new PaymentDeclined(result.status); // the vendor's status string becomes our exception
return result.id;
}
refund(paymentRef, amount) { this.sdk.refundCharge(paymentRef, amount * 100); }
}
class BetaAdapter {
constructor(sdk) { this.sdk = sdk; }
charge(customer, amount, idempotencyKey) {
const receipt = this.sdk.pay("card_" + customer, amount, idempotencyKey); // our key is the vendor's reference
if (receipt.code !== 0) throw new PaymentDeclined(`code ${receipt.code}`); // the vendor's integer code becomes our exception
return receipt.number;
}
refund(paymentRef, amount) { this.sdk.reverse(paymentRef, amount); }
}
class FakeGateway { // a test double is a third implementation of the same target
constructor(broke) {
this.broke = new Set(broke);
this.refs = new Map();
}
charge(customer, amount, idempotencyKey) {
if (this.broke.has(customer)) throw new PaymentDeclined("no funds");
if (!this.refs.has(idempotencyKey)) this.refs.set(idempotencyKey, `fake_${this.refs.size + 1}`);
return this.refs.get(idempotencyKey);
}
refund(paymentRef, amount) {}
}
class Checkout { // the client: it names no vendor, no unit and no status code
constructor(gateway) {
this.gateway = gateway;
this.refs = new Map();
}
pay(orderId, customer, amount) {
let ref;
try {
ref = this.gateway.charge(customer, amount, orderId);
} catch (e) {
if (!(e instanceof PaymentDeclined)) throw e;
return `declined ${amount}`;
}
this.refs.set(orderId, ref);
return `paid ${amount}, ref ${ref}`;
}
refund(orderId, amount) {
this.gateway.refund(this.refs.get(orderId), amount);
return `refunded ${amount} of ${orderId}`;
}
}
function run(label, checkout) { // one script drives every gateway
console.log(`${label}: ${checkout.pay("o-1", "asha", 499)}`);
console.log(`${label}: retry with the same key: ${checkout.pay("o-1", "asha", 499)}`);
console.log(`${label}: ${checkout.pay("o-2", "ravi", 499)}`);
console.log(`${label}: ${checkout.refund("o-1", 100)}`);
}
const alpha = new AlphaSdk({ tok_asha: 100000, tok_ravi: 30000 });
const beta = new BetaSdk({ card_asha: 1000, card_ravi: 300 });
run("alpha", new Checkout(new AlphaAdapter(alpha)));
run("beta", new Checkout(new BetaAdapter(beta)));
run("fake", new Checkout(new FakeGateway(["ravi"])));
console.log(`units the SDKs saw: alpha ${alpha.lastPaise} paise, beta ${beta.lastRupees} rupees`);alpha: paid 499, ref al_1
alpha: retry with the same key: paid 499, ref al_1
alpha: declined 499
alpha: refunded 100 of o-1
beta: paid 499, ref bt_1
beta: retry with the same key: paid 499, ref bt_1
beta: declined 499
beta: refunded 100 of o-1
fake: paid 499, ref fake_1
fake: retry with the same key: paid 499, ref fake_1
fake: declined 499
fake: refunded 100 of o-1
units the SDKs saw: alpha 49900 paise, beta 499 rupeesCheck one row by hand. Alpha: the checkout asks for 499 rupees, AlphaAdapter sends 499 × 100 = 49,900 paise, Asha has 100,000 paise, so the SDK answers succeeded with al_1 and the adapter returns that reference. Ravi has 30,000 paise, 30,000 is less than 49,900, so the SDK answers declined and the adapter raises PaymentDeclined, which the checkout turns into declined 499. Beta is the same story with 499 and the code 51. One run function drives all three gateways, and the output has the same shape for each. That is what the target buys you.
Each language writes the same shape in its own way:
- Python has
ABCfor the target, and because of duck typing atyping.Protocolwould do just as well: an adapter doesn't even have to inherit from the target. - Java uses an
interface, and the adaptersimplementit. The adapter throws an uncheckedPaymentDeclinedso the interface needs nothrowsclause. Vendor SDKs often throw checked exceptions of their own, and the adapter is where those get translated. - C++ uses an abstract base class with a virtual destructor. The adapter holds a reference to the SDK, so the SDK must outlive it; hold a
shared_ptrif ownership is shared. - JavaScript has no interface keyword, so the target is a convention: any object with
chargeandrefundfits. An adapter can be a class, as here, or a function that returns an object.
Only the adapter knows the vendor: it converts rupees to paise on the way in, and the vendor's answer into a reference or an exception coming out.
Object adapter and class adapter#
The book describes two forms of Adapter. An object adapter holds an instance of the adaptee and forwards to it: composition, "has a", which is what every adapter above does. A class adapter inherits from the adaptee and implements the target: "is a". The book's version needs multiple inheritance, because the adapter inherits publicly from the target and privately from the adaptee. Whether you can write one depends on the language:
| Object adapter (holds the SDK) | Class adapter (inherits the SDK) | |
|---|---|---|
| Python | The default | Possible: class A(Target, Adaptee). The SDK's methods become the adapter's own, and the two interfaces share one namespace, so a clash of names is yours to resolve. Rarely worth it: duck typing means the adapter needn't inherit anything |
| Java | The default | Only if the SDK class is not final and the target is an interface: extends Sdk implements Target. Vendor classes are often final, and a target that is a class rules it out (one superclass only) |
| C++ | The default | The book's own form: public Target, private Adaptee. Private inheritance keeps the SDK's interface hidden from clients, but the adapter is welded to one concrete SDK class |
| JavaScript | The default (a class, or a function that returns an object) | class A extends Sdk works, and there is no interface to implement. Rare |
Only C++ can keep the inheritance to itself. In Python, Java and JavaScript every public method of the SDK is also a public method of the class adapter, so code that holds the concrete AlphaClassAdapter can call create_charge and reach around the target. That is one more reason the object adapter is the default.
The same Alpha vendor both ways, with the client unable to tell them apart:
# class_adapter.py: an object adapter and a class adapter for the same SDK: the client cannot tell them apart
from abc import ABC, abstractmethod
class AlphaCharge:
def __init__(self, id: str, status: str) -> None:
self.id, self.status = id, status
class AlphaSdk: # vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
def __init__(self, balances) -> None:
self.balances, self.charges, self.last_paise = dict(balances), {}, 0
def create_charge(self, token: str, paise: int, idempotency_key: str) -> AlphaCharge:
self.last_paise = paise
if idempotency_key not in self.charges:
ok = self.balances.get(token, 0) >= paise
if ok:
self.balances[token] -= paise
self.charges[idempotency_key] = AlphaCharge(f"al_{len(self.charges) + 1}" if ok else "", "succeeded" if ok else "declined")
return self.charges[idempotency_key]
def refund_charge(self, charge_id: str, paise: int) -> str:
return "refunded"
class PaymentDeclined(Exception):
pass
class PaymentGateway(ABC): # the target: what the checkout needs, in the checkout's own terms (rupees, one ref, one exception)
@abstractmethod
def charge(self, customer: str, amount: int, idempotency_key: str) -> str: ...
@abstractmethod
def refund(self, payment_ref: str, amount: int) -> None: ...
class AlphaAdapter(PaymentGateway): # an object adapter: it HAS an SDK
def __init__(self, sdk: AlphaSdk) -> None:
self.sdk = sdk
def charge(self, customer: str, amount: int, idempotency_key: str) -> str:
result = self.sdk.create_charge("tok_" + customer, amount * 100, idempotency_key) # rupees become paise
if result.status != "succeeded":
raise PaymentDeclined(result.status)
return result.id
def refund(self, payment_ref: str, amount: int) -> None:
self.sdk.refund_charge(payment_ref, amount * 100)
class AlphaClassAdapter(PaymentGateway, AlphaSdk): # a class adapter: it IS an SDK (multiple inheritance)
def charge(self, customer: str, amount: int, idempotency_key: str) -> str:
result = self.create_charge("tok_" + customer, amount * 100, idempotency_key) # its own inherited method
if result.status != "succeeded":
raise PaymentDeclined(result.status)
return result.id
def refund(self, payment_ref: str, amount: int) -> None:
self.refund_charge(payment_ref, amount * 100)
class Checkout: # the client: it names no vendor, no unit and no status code
def __init__(self, gateway: PaymentGateway) -> None:
self.gateway, self.refs = gateway, {}
def pay(self, order_id: str, customer: str, amount: int) -> str:
try:
ref = self.gateway.charge(customer, amount, order_id)
except PaymentDeclined:
return f"declined {amount}"
self.refs[order_id] = ref
return f"paid {amount}, ref {ref}"
def refund(self, order_id: str, amount: int) -> str:
self.gateway.refund(self.refs[order_id], amount)
return f"refunded {amount} of {order_id}"
def run(label: str, checkout: Checkout) -> None:
print(f"{label}: {checkout.pay('o-1', 'asha', 499)}")
print(f"{label}: retry with the same key: {checkout.pay('o-1', 'asha', 499)}")
print(f"{label}: {checkout.pay('o-2', 'ravi', 499)}")
print(f"{label}: {checkout.refund('o-1', 100)}")
balances = {"tok_asha": 100000, "tok_ravi": 30000}
by_object = AlphaAdapter(AlphaSdk(balances))
by_class = AlphaClassAdapter(balances) # the adapter is built like the SDK itself
run("object adapter", Checkout(by_object))
run("class adapter", Checkout(by_class))
print(f"units the SDKs saw: object {by_object.sdk.last_paise} paise, class {by_class.last_paise} paise")// ClassAdapter.java: an object adapter and a class adapter for the same SDK: the client cannot tell them apart
import java.util.HashMap;
import java.util.Map;
class ClassAdapter {
record AlphaCharge(String id, String status) {}
static class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
final Map<String, Integer> balances;
final Map<String, AlphaCharge> charges = new HashMap<>();
int lastPaise = 0;
AlphaSdk(Map<String, Integer> balances) { this.balances = new HashMap<>(balances); }
AlphaCharge createCharge(String token, int paise, String idempotencyKey) {
lastPaise = paise;
if (!charges.containsKey(idempotencyKey)) {
boolean ok = balances.getOrDefault(token, 0) >= paise;
if (ok) balances.merge(token, -paise, Integer::sum);
charges.put(idempotencyKey, new AlphaCharge(ok ? "al_" + (charges.size() + 1) : "", ok ? "succeeded" : "declined"));
}
return charges.get(idempotencyKey);
}
String refundCharge(String chargeId, int paise) { return "refunded"; }
}
static final class PaymentDeclined extends RuntimeException {
PaymentDeclined(String why) { super(why); }
}
interface PaymentGateway { // the target: what the checkout needs, in the checkout's own terms (rupees, one ref, one exception)
String charge(String customer, int amount, String idempotencyKey);
void refund(String paymentRef, int amount);
}
static final class AlphaAdapter implements PaymentGateway { // an object adapter: it HAS an SDK
final AlphaSdk sdk;
AlphaAdapter(AlphaSdk sdk) { this.sdk = sdk; }
public String charge(String customer, int amount, String idempotencyKey) {
AlphaCharge result = sdk.createCharge("tok_" + customer, amount * 100, idempotencyKey); // rupees become paise
if (!result.status().equals("succeeded")) throw new PaymentDeclined(result.status());
return result.id();
}
public void refund(String paymentRef, int amount) { sdk.refundCharge(paymentRef, amount * 100); }
}
static final class AlphaClassAdapter extends AlphaSdk implements PaymentGateway { // a class adapter: it IS an SDK (needs a non-final SDK class)
AlphaClassAdapter(Map<String, Integer> balances) { super(balances); }
public String charge(String customer, int amount, String idempotencyKey) {
AlphaCharge result = createCharge("tok_" + customer, amount * 100, idempotencyKey); // its own inherited method
if (!result.status().equals("succeeded")) throw new PaymentDeclined(result.status());
return result.id();
}
public void refund(String paymentRef, int amount) { refundCharge(paymentRef, amount * 100); }
}
static final class Checkout { // the client: it names no vendor, no unit and no status code
private final PaymentGateway gateway;
private final Map<String, String> refs = new HashMap<>();
Checkout(PaymentGateway gateway) { this.gateway = gateway; }
String pay(String orderId, String customer, int amount) {
String ref;
try {
ref = gateway.charge(customer, amount, orderId);
} catch (PaymentDeclined e) {
return "declined " + amount;
}
refs.put(orderId, ref);
return "paid " + amount + ", ref " + ref;
}
String refund(String orderId, int amount) {
gateway.refund(refs.get(orderId), amount);
return "refunded " + amount + " of " + orderId;
}
}
static void run(String label, Checkout checkout) {
System.out.println(label + ": " + checkout.pay("o-1", "asha", 499));
System.out.println(label + ": retry with the same key: " + checkout.pay("o-1", "asha", 499));
System.out.println(label + ": " + checkout.pay("o-2", "ravi", 499));
System.out.println(label + ": " + checkout.refund("o-1", 100));
}
public static void main(String[] args) {
Map<String, Integer> balances = Map.of("tok_asha", 100000, "tok_ravi", 30000);
AlphaAdapter byObject = new AlphaAdapter(new AlphaSdk(balances));
AlphaClassAdapter byClass = new AlphaClassAdapter(balances); // the adapter is built like the SDK itself
run("object adapter", new Checkout(byObject));
run("class adapter", new Checkout(byClass));
System.out.println("units the SDKs saw: object " + byObject.sdk.lastPaise + " paise, class " + byClass.lastPaise + " paise");
}
}// class_adapter.cpp: an object adapter and a class adapter for the same SDK: the client cannot tell them apart
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <utility>
struct AlphaCharge {
std::string id;
std::string status;
};
class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
std::map<std::string, int> balances_;
std::map<std::string, AlphaCharge> charges_;
public:
int last_paise = 0;
explicit AlphaSdk(std::map<std::string, int> balances) : balances_(std::move(balances)) {}
AlphaCharge create_charge(const std::string& token, int paise, const std::string& idempotency_key) {
last_paise = paise;
if (charges_.count(idempotency_key) == 0) {
bool ok = balances_[token] >= paise;
if (ok) balances_[token] -= paise;
charges_[idempotency_key] = AlphaCharge{ok ? "al_" + std::to_string(charges_.size() + 1) : "", ok ? "succeeded" : "declined"};
}
return charges_[idempotency_key];
}
std::string refund_charge(const std::string& /*charge_id*/, int /*paise*/) { return "refunded"; }
};
class PaymentDeclined : public std::runtime_error {
public:
explicit PaymentDeclined(const std::string& why) : std::runtime_error(why) {}
};
class PaymentGateway { // the target: what the checkout needs, in the checkout's own terms (rupees, one ref, one exception)
public:
virtual ~PaymentGateway() = default;
virtual std::string charge(const std::string& customer, int amount, const std::string& idempotency_key) = 0;
virtual void refund(const std::string& payment_ref, int amount) = 0;
};
class AlphaAdapter : public PaymentGateway { // an object adapter: it HAS an SDK
AlphaSdk& sdk_;
public:
explicit AlphaAdapter(AlphaSdk& sdk) : sdk_(sdk) {}
std::string charge(const std::string& customer, int amount, const std::string& idempotency_key) override {
AlphaCharge result = sdk_.create_charge("tok_" + customer, amount * 100, idempotency_key); // rupees become paise
if (result.status != "succeeded") throw PaymentDeclined(result.status);
return result.id;
}
void refund(const std::string& payment_ref, int amount) override { sdk_.refund_charge(payment_ref, amount * 100); }
};
class AlphaClassAdapter : public PaymentGateway, private AlphaSdk { // a class adapter: it IS an SDK, but private inheritance keeps that to itself
public:
using AlphaSdk::last_paise;
explicit AlphaClassAdapter(std::map<std::string, int> balances) : AlphaSdk(std::move(balances)) {}
std::string charge(const std::string& customer, int amount, const std::string& idempotency_key) override {
AlphaCharge result = create_charge("tok_" + customer, amount * 100, idempotency_key); // its own inherited member
if (result.status != "succeeded") throw PaymentDeclined(result.status);
return result.id;
}
void refund(const std::string& payment_ref, int amount) override { refund_charge(payment_ref, amount * 100); }
};
class Checkout { // the client: it names no vendor, no unit and no status code
PaymentGateway& gateway_;
std::map<std::string, std::string> refs_;
public:
explicit Checkout(PaymentGateway& gateway) : gateway_(gateway) {}
std::string pay(const std::string& order_id, const std::string& customer, int amount) {
std::string ref;
try {
ref = gateway_.charge(customer, amount, order_id);
} catch (const PaymentDeclined&) {
return "declined " + std::to_string(amount);
}
refs_[order_id] = ref;
return "paid " + std::to_string(amount) + ", ref " + ref;
}
std::string refund(const std::string& order_id, int amount) {
gateway_.refund(refs_[order_id], amount);
return "refunded " + std::to_string(amount) + " of " + order_id;
}
};
void run(const std::string& label, Checkout checkout) {
std::cout << label << ": " << checkout.pay("o-1", "asha", 499) << "\n";
std::cout << label << ": retry with the same key: " << checkout.pay("o-1", "asha", 499) << "\n";
std::cout << label << ": " << checkout.pay("o-2", "ravi", 499) << "\n";
std::cout << label << ": " << checkout.refund("o-1", 100) << "\n";
}
int main() {
std::map<std::string, int> balances = {{"tok_asha", 100000}, {"tok_ravi", 30000}};
AlphaSdk sdk(balances);
AlphaAdapter by_object(sdk);
AlphaClassAdapter by_class(balances); // the adapter is built like the SDK itself
run("object adapter", Checkout(by_object));
run("class adapter", Checkout(by_class));
std::cout << "units the SDKs saw: object " << sdk.last_paise << " paise, class " << by_class.last_paise << " paise\n";
}// classAdapter.js: an object adapter and a class adapter for the same SDK: the client cannot tell them apart
class AlphaSdk { // vendor A, as shipped: paise, a status string, a charge id; the same key gives the same charge
constructor(balances) {
this.balances = { ...balances };
this.charges = new Map();
this.lastPaise = 0;
}
createCharge(token, paise, idempotencyKey) {
this.lastPaise = paise;
if (!this.charges.has(idempotencyKey)) {
const ok = (this.balances[token] ?? 0) >= paise;
if (ok) this.balances[token] -= paise;
this.charges.set(idempotencyKey, { id: ok ? `al_${this.charges.size + 1}` : "", status: ok ? "succeeded" : "declined" });
}
return this.charges.get(idempotencyKey);
}
refundCharge(chargeId, paise) { return "refunded"; }
}
// the target has no interface keyword in JavaScript: any object with charge(customer, amount, key) and refund(ref, amount) fits
class PaymentDeclined extends Error {}
class AlphaAdapter { // an object adapter: it HAS an SDK
constructor(sdk) { this.sdk = sdk; }
charge(customer, amount, idempotencyKey) {
const result = this.sdk.createCharge("tok_" + customer, amount * 100, idempotencyKey); // rupees become paise
if (result.status !== "succeeded") throw new PaymentDeclined(result.status);
return result.id;
}
refund(paymentRef, amount) { this.sdk.refundCharge(paymentRef, amount * 100); }
}
class AlphaClassAdapter extends AlphaSdk { // a class adapter: it IS an SDK (single inheritance is enough here)
charge(customer, amount, idempotencyKey) {
const result = this.createCharge("tok_" + customer, amount * 100, idempotencyKey); // its own inherited method
if (result.status !== "succeeded") throw new PaymentDeclined(result.status);
return result.id;
}
refund(paymentRef, amount) { this.refundCharge(paymentRef, amount * 100); }
}
class Checkout { // the client: it names no vendor, no unit and no status code
constructor(gateway) {
this.gateway = gateway;
this.refs = new Map();
}
pay(orderId, customer, amount) {
let ref;
try {
ref = this.gateway.charge(customer, amount, orderId);
} catch (e) {
if (!(e instanceof PaymentDeclined)) throw e;
return `declined ${amount}`;
}
this.refs.set(orderId, ref);
return `paid ${amount}, ref ${ref}`;
}
refund(orderId, amount) {
this.gateway.refund(this.refs.get(orderId), amount);
return `refunded ${amount} of ${orderId}`;
}
}
function run(label, checkout) {
console.log(`${label}: ${checkout.pay("o-1", "asha", 499)}`);
console.log(`${label}: retry with the same key: ${checkout.pay("o-1", "asha", 499)}`);
console.log(`${label}: ${checkout.pay("o-2", "ravi", 499)}`);
console.log(`${label}: ${checkout.refund("o-1", 100)}`);
}
const balances = { tok_asha: 100000, tok_ravi: 30000 };
const byObject = new AlphaAdapter(new AlphaSdk(balances));
const byClass = new AlphaClassAdapter(balances); // the adapter is built like the SDK itself
run("object adapter", new Checkout(byObject));
run("class adapter", new Checkout(byClass));
console.log(`units the SDKs saw: object ${byObject.sdk.lastPaise} paise, class ${byClass.lastPaise} paise`);object adapter: paid 499, ref al_1
object adapter: retry with the same key: paid 499, ref al_1
object adapter: declined 499
object adapter: refunded 100 of o-1
class adapter: paid 499, ref al_1
class adapter: retry with the same key: paid 499, ref al_1
class adapter: declined 499
class adapter: refunded 100 of o-1
units the SDKs saw: object 49900 paise, class 49900 paiseRead the two blocks of output: identical. The difference is in what each form can and can't do. The book lists the trade-offs: a class adapter commits to one concrete adaptee class, so it can't adapt a class and all its subclasses; it lets the adapter override some of the adaptee's behaviour; and it is a single object, with no extra indirection. An object adapter works with many adaptees (the SDK class and any subclass of it), but to change an adaptee's behaviour you have to subclass the adaptee and hand the subclass to the adapter.
Object adapter, the default, when
- You are handed the SDK object: injected, pooled or already configured at start-up
- The SDK class is
final, or you want to adapt it and its subclasses - Clients must see only the target's methods and not the SDK's
- The language gives you one superclass and the target is a class
Class adapter, when
- You create the SDK object yourself and are allowed to extend its class
- You must override one of its methods, or reach a protected member
- One object instead of two matters
Which form?
Start-up code builds one configured AlphaSdk, with its credentials and connection pool, and hands it to you. You must expose it as a PaymentGateway. Object adapter or class adapter?
One strong answer
An object adapter. A class adapter is the SDK object, so you would have to create it yourself: you can't turn an instance that already exists into an instance of your subclass. An object adapter just holds the reference it is given, and the configuration and the pool stay where they are.
An object adapter holds the SDK it is given. A class adapter is the SDK, so you create it yourself from a class you may extend.
What an adapter should translate, and what it should not#
An adapter has five legitimate jobs, and each one is visible in the code above:
- Names:
create_chargebecomescharge. - Units: rupees become paise. Other classic ones are seconds and milliseconds, and metres and feet.
- Shapes: a tuple, a record or a dictionary becomes your own result.
- Errors: a status string or an integer code becomes your exception, so the vendor's exception classes never escape.
- Identifiers: a customer becomes a vendor token, and your order id becomes the vendor's key.
And some things are not its job. Each is a way to quietly lose the benefit:
- Business rules. A refund limit or a fee belongs in your domain, not in the class that is replaced whenever you change vendor.
- Behaviour added around the call, such as retries and logging. That is wrapping a class that already has your interface: the Decorator in the next lesson.
- Capabilities the vendor lacks. If a vendor has no idempotency key, the adapter can't invent a safe retry without storing keys of its own, and at that point you are writing a small service, not an adapter. Say so out loud instead of hiding it.
- The whole vendor API. Adapt the two methods the checkout uses, not the vendor's forty. That is the client-shaped interface from the interface-segregation lesson.
Two habits keep adapters honest. Test the adapter against the vendor's sandbox, with a small contract test: the adapter's one risk is translating wrongly, and a fake can't catch that. And treat fakes as adapters too: FakeGateway is a third class behind the same target, which is why the checkout is testable without a vendor.
A leaky adapter hands the vendor's status string to the checkout, so the checkout learns the vendor's vocabulary. A good adapter returns a reference, or raises one exception.
Adapter and its look-alikes#
| Pattern | What it does | How to tell it from Adapter |
|---|---|---|
| Facade | Defines a new, simpler interface over several classes | An adapter makes one class fit an interface that already exists; a facade invents the interface |
| Decorator | Same interface in, same interface out, adds behaviour | A decorator keeps the interface; an adapter changes it |
| Proxy | Same interface as the real object, controls access to it | A proxy and its subject look the same to the client, and its reason is control (lazy, cached, guarded, remote). An adapter and its adaptee look different, and its reason is translation |
| Bridge | Splits an abstraction from its implementation so both vary | The book's contrast: Adapter makes things work after they are designed, Bridge makes them work before |
| Strategy | Interchangeable ways of doing one job | An adapter can be a strategy, as AlphaAdapter is one implementation chosen at start-up, but a strategy is designed to fit and an adapter is a retrofit |
| Ports and adapters (hexagonal architecture) | The same idea for a whole application | Your interfaces are the ports, and one adapter per technology plugs into each. Domain-driven design calls a translating layer between two models an anti-corruption layer |
When not to use it#
- When you own the code. If the class is yours, change its interface or rename the method. An adapter is for code you can't or shouldn't edit.
- For one call used once. A function that calls
sdk.create_charge(...)is enough. A class, an interface and a fake are worth it when there are two vendors, or when you need to test around one. - When it grows rules. An adapter that decides, retries, caches or logs is becoming a service. Split it: the translation stays, the rest wraps it.
- When the adapter adapts an adapter. A chain of them means the interfaces were never settled. Fix the target.
- When it hides a bad fit. If the vendor's model really differs from yours (an asynchronous callback against your synchronous call), the adapter will be large and leaky, and the real work is deciding what your interface should promise.
Where it shows up in the case studies#
In all three the interface is the seam, and the in-memory class the round writes stands in for what an adapter would do in production:
| Case study | Interface | What an adapter would do |
|---|---|---|
| Movie booking | SeatLockProvider (lock, holds, unlock, is_held), implemented by InMemorySeatLocks |
The lesson's table says "Strategy / adapter". The production version is Redis: one key per seat, SET seat:{show}:{seat} {booking_id} NX PX 600000, all seats in one Lua script. That class translates the four calls into Redis commands, and BookingService doesn't change |
| ATM | Bank (verify_pin, balance, debit, reverse), implemented by InMemoryBank and FlakyBank |
"The ATM talks to a bank switch in production and to InMemoryBank in the round." The production class translates the four calls into the switch's messages. FlakyBank is a test double that injects timeouts |
| Food delivery | PaymentGateway (charge(customer, amount, idempotency_key), refund(payment_ref, amount)), implemented by FakeGateway |
The lesson's words: "the real gateway's SDK hides behind two methods; the demo uses a fake that records calls". The interface is the one this lesson built |
In the interview#
Say it with the reason: "Adapter wraps a class I can't change so that it implements the interface my code expects. I translate names, units and errors in that one class, so the client and its tests never see the vendor, and a new vendor is a new class."
Then expect the follow-ups:
- "Adapter or Facade?" An adapter makes one class fit an interface that already exists. A facade gives several classes one new, simpler interface.
- "Object adapter or class adapter?" Object adapter by default: it holds the adaptee, so it can wrap an instance it was given, and works in every language. A class adapter inherits the adaptee, which needs multiple inheritance (C++, Python) or, in Java, a non-final SDK class and an interface target. It welds you to one concrete type, and outside C++'s private inheritance it exposes the SDK's methods too.
- "How do you test it?" The client with a fake adapter, and each real adapter with a contract test against the vendor's sandbox.
- "Where does it live?" At the edge: it is the only code that imports the vendor SDK, and the composition root is the only place that chooses it.
- "The vendor ships a breaking release." Only the adapter changes.
- "How is it different from a Decorator?" A decorator keeps the interface and adds behaviour. An adapter changes the interface.
Common mistakes#
- A target that mirrors the vendor. The interface returns the vendor's result type or status string, so the checkout still depends on the vendor.
- Leaking the vendor's exceptions through the adapter instead of translating them.
- Forgetting a unit conversion in one of the two directions (rupees to paise on the way in, paise to rupees on the way out).
- Business rules inside the adapter, so changing vendor changes your pricing.
- Choosing a class adapter for an SDK object someone else already created, or for a
finalclass. - Calling a facade an adapter (or the reverse) when asked which pattern it is.
Adapter
Q1/4 Start-up code builds one configured vendor SDK object and hands it to you. You must expose it as your own PaymentGateway interface. Which form do you use?
Answer & explanation
Answer: B
A class adapter is the SDK object, so you would create it yourself. You can't turn an existing instance into an instance of your subclass. An object adapter just holds the reference it is given.
Q2/4 An adapter's charge method returns the vendor's status string so the checkout can show it. What is wrong with that?
Answer & explanation
Answer: C
The adapter's job is to translate the vendor's habits into the target's terms: one reference, or one exception. A leaked status string moves the vendor knowledge back into the client.
Q3/4 What is the difference between Adapter and Facade?
Answer & explanation
Answer: C
The adapter's target interface comes first, and the adapter is written to fit it. A facade invents a simpler interface over a subsystem of several classes. The facade lesson (lesson 12) takes it from there.
Q4/4 Where should a vendor's units and status codes be translated?
Answer & explanation
Answer: C
Translating at one edge means a vendor change touches one class, and the client and its tests never learn the vendor's vocabulary.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


