Adapter
An adapter sits between code and an object it can't use directly, translating calls and answers so neither side has to change. This page plugs two payment providers with clashing APIs into Pedal's checkout, and follows a charge through each adapter.
1. The pain
Pedal's checkout was written against one idea of payment: charge a rider some pence, get back "charged" with a reference or "declined" with a reason. Then finance signed two payment providers (their names here are invented). Lumen Pay wants pounds as a string like "2.80" and answers with a status dictionary. Orbit Cards wants minor units plus a currency code, and throws an exception when it declines.
Calling the SDKs directly spreads their quirks through checkout, refunds and monthly memberships: unit conversions in three places, two styles of error handling side by side, and every future provider adding a third. We can't change the SDKs; we don't want to change checkout.
2. The idea in one picture
Adapter wraps an object whose interface doesn't fit and presents the interface the client expects. Checkout keeps calling charge(rider, pence); the Lumen adapter turns that into make_payment("2.80", rider) and turns the status dictionary back into a result. The Orbit adapter passes pence through as minor units and turns an exception into a "declined" result.
An adapter translates and nothing more: units, names, argument order, error styles. If it starts adding business rules, it's become something else. The version here holds the SDK as a field (an object adapter); a class adapter inherits from it instead, which few languages make pleasant.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Target | The interface the client wants to use. | PaymentGateway.charge(rider, pence) |
| Adaptee | The existing object with the wrong interface. | LumenPaySdk, OrbitCardsSdk |
| Adapter | Implements the target by translating calls to the adaptee. | LumenAdapter, OrbitAdapter |
| Client | Uses only the target. | checkout() |
4. Before and after
Checkout both ways (Python sketch; the full program is in section 5).
Without the pattern
def checkout(provider, rider, pence):
if provider == "lumen":
r = lumen.make_payment(
f"{pence // 100}.{pence % 100:02d}",
rider)
ok = r["status"] == "ok"
else:
try:
orbit.debit(rider, pence, "GBP")
ok = True
except OrbitError:
ok = False
# refunds() repeats all of thisWith it
class LumenAdapter(PaymentGateway):
def charge(self, rider, pence):
r = self.sdk.make_payment(
f"{pence // 100}.{pence % 100:02d}",
rider)
return ChargeResult(
r["status"] == "ok",
r.get("ref") or r["reason"])
def checkout(gateway, rider, pence):
return gateway.charge(rider, pence)| The next change | Without the pattern | With it |
|---|---|---|
| Sign a third provider | A new branch in checkout, refunds and memberships | A new adapter; nothing else changes |
| Lumen's SDK renames a field | Find every place that reads its dictionary | One line in LumenAdapter |
| Test checkout without a real provider | Mock the SDK's exact quirks | A tiny fake PaymentGateway |
Each provider's strangeness lives in exactly one class, next to the SDK it explains.
5. The code
Two provider SDKs we can't change, one adapter each implementing PaymentGateway, and a checkout() written once. The last two calls are declined: one by a status field, one by an exception.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
# ---- What Pedal's checkout wants: pence in, a clear result out. ----
@dataclass
class ChargeResult:
ok: bool
detail: str # a reference when ok, a reason when not
class PaymentGateway(ABC):
@abstractmethod
def charge(self, rider: str, pence: int) -> ChargeResult: ...
# ---- Two providers' SDKs, as they come. We can't change them. ----
class LumenPaySdk:
"""Takes pounds as a string, answers with a status dictionary."""
def __init__(self) -> None:
self.next_ref = 1001
def make_payment(self, amount: str, customer: str) -> dict[str, str]:
if float(amount) > 500:
return {"status": "declined", "reason": "insufficient funds"}
ref, self.next_ref = f"LP-{self.next_ref}", self.next_ref + 1
return {"status": "ok", "ref": ref}
class OrbitError(Exception):
pass
class OrbitTxn:
def __init__(self, txn_id: int) -> None:
self.txn_id = txn_id
class OrbitCardsSdk:
"""Takes minor units and a currency code, raises on failure."""
def __init__(self) -> None:
self.count = 0
def debit(self, account: str, minor_units: int, currency: str) -> OrbitTxn:
if minor_units > 50_000:
raise OrbitError("limit exceeded")
self.count += 1
return OrbitTxn(self.count)
# ---- Adapters: each makes one SDK look like a PaymentGateway. ----
class LumenAdapter(PaymentGateway):
def __init__(self, sdk: LumenPaySdk) -> None:
self.sdk = sdk
def charge(self, rider: str, pence: int) -> ChargeResult:
reply = self.sdk.make_payment(f"{pence // 100}.{pence % 100:02d}", rider) # pence -> "2.80"
if reply["status"] == "ok":
return ChargeResult(True, reply["ref"])
return ChargeResult(False, reply["reason"])
class OrbitAdapter(PaymentGateway):
def __init__(self, sdk: OrbitCardsSdk) -> None:
self.sdk = sdk
def charge(self, rider: str, pence: int) -> ChargeResult:
try:
txn = self.sdk.debit(rider, pence, "GBP") # pence are already minor units
return ChargeResult(True, f"ORB-{txn.txn_id}")
except OrbitError as e: # exception -> result
return ChargeResult(False, str(e))
# ---- The client: written once, against the interface it wanted. ----
def checkout(gateway: PaymentGateway, name: str, rider: str, pence: int) -> str:
r = gateway.charge(rider, pence)
return f"checkout via {name}: {pence}p " + (f"charged, ref {r.detail}" if r.ok else f"declined ({r.detail})")
if __name__ == "__main__":
gateways: list[tuple[str, PaymentGateway]] = [("lumen", LumenAdapter(LumenPaySdk())), ("orbit", OrbitAdapter(OrbitCardsSdk()))]
for name, gw in gateways:
print(checkout(gw, name, "rider-7", 280))
for name, gw in gateways:
print(checkout(gw, name, "rider-7", 99_999))
// ---- What Pedal's checkout wants: pence in, a clear result out. ----
interface ChargeResult { ok: boolean; detail: string; } // detail: a reference when ok, a reason when not
interface PaymentGateway { charge(rider: string, pence: number): ChargeResult; }
// ---- Two providers' SDKs, as they come. We can't change them. ----
class LumenPaySdk { // takes pounds as a string, answers with a status object
private nextRef = 1001;
makePayment(amount: string, customer: string): Record<string, string> {
void customer;
if (Number(amount) > 500) return { status: "declined", reason: "insufficient funds" };
return { status: "ok", ref: `LP-${this.nextRef++}` };
}
}
class OrbitError extends Error {}
class OrbitTxn { constructor(readonly txnId: number) {} }
class OrbitCardsSdk { // takes minor units and a currency code, throws on failure
private count = 0;
debit(account: string, minorUnits: number, currency: string): OrbitTxn {
void account; void currency;
if (minorUnits > 50_000) throw new OrbitError("limit exceeded");
return new OrbitTxn(++this.count);
}
}
// ---- Adapters: each makes one SDK look like a PaymentGateway. ----
class LumenAdapter implements PaymentGateway {
constructor(private readonly sdk: LumenPaySdk) {}
charge(rider: string, pence: number): ChargeResult {
const reply = this.sdk.makePayment(`${Math.floor(pence / 100)}.${String(pence % 100).padStart(2, "0")}`, rider); // pence -> "2.80"
return reply.status === "ok" ? { ok: true, detail: reply.ref } : { ok: false, detail: reply.reason };
}
}
class OrbitAdapter implements PaymentGateway {
constructor(private readonly sdk: OrbitCardsSdk) {}
charge(rider: string, pence: number): ChargeResult {
try {
const txn = this.sdk.debit(rider, pence, "GBP"); // pence are already minor units
return { ok: true, detail: `ORB-${txn.txnId}` };
} catch (e) {
if (e instanceof OrbitError) return { ok: false, detail: e.message }; // exception -> result
throw e;
}
}
}
// ---- The client: written once, against the interface it wanted. ----
function checkout(gateway: PaymentGateway, name: string, rider: string, pence: number): string {
const r = gateway.charge(rider, pence);
return `checkout via ${name}: ${pence}p ` + (r.ok ? `charged, ref ${r.detail}` : `declined (${r.detail})`);
}
const gateways: [string, PaymentGateway][] = [["lumen", new LumenAdapter(new LumenPaySdk())], ["orbit", new OrbitAdapter(new OrbitCardsSdk())]];
for (const [name, gw] of gateways) console.log(checkout(gw, name, "rider-7", 280));
for (const [name, gw] of gateways) console.log(checkout(gw, name, "rider-7", 99_999));import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
// ---- What Pedal's checkout wants: pence in, a clear result out. ----
record ChargeResult(boolean ok, String detail) {} // detail: a reference when ok, a reason when not
interface PaymentGateway { ChargeResult charge(String rider, int pence); }
// ---- Two providers' SDKs, as they come. We can't change them. ----
static class LumenPaySdk { // takes pounds as a string, answers with a status map
private int nextRef = 1001;
Map<String, String> makePayment(String amount, String customer) {
if (new BigDecimal(amount).compareTo(new BigDecimal(500)) > 0) return Map.of("status", "declined", "reason", "insufficient funds");
return Map.of("status", "ok", "ref", "LP-" + nextRef++);
}
}
static class OrbitException extends Exception {
private static final long serialVersionUID = 1L;
OrbitException(String message) { super(message); }
}
record OrbitTxn(int txnId) {}
static class OrbitCardsSdk { // takes minor units and a currency code, throws on failure
private int count;
OrbitTxn debit(String account, int minorUnits, String currency) throws OrbitException {
if (minorUnits > 50_000) throw new OrbitException("limit exceeded");
return new OrbitTxn(++count);
}
}
// ---- Adapters: each makes one SDK look like a PaymentGateway. ----
static class LumenAdapter implements PaymentGateway {
private final LumenPaySdk sdk;
LumenAdapter(LumenPaySdk sdk) { this.sdk = sdk; }
public ChargeResult charge(String rider, int pence) {
Map<String, String> reply = sdk.makePayment(String.format("%d.%02d", pence / 100, pence % 100), rider); // pence -> "2.80"
return reply.get("status").equals("ok") ? new ChargeResult(true, reply.get("ref")) : new ChargeResult(false, reply.get("reason"));
}
}
static class OrbitAdapter implements PaymentGateway {
private final OrbitCardsSdk sdk;
OrbitAdapter(OrbitCardsSdk sdk) { this.sdk = sdk; }
public ChargeResult charge(String rider, int pence) {
try {
OrbitTxn txn = sdk.debit(rider, pence, "GBP"); // pence are already minor units
return new ChargeResult(true, "ORB-" + txn.txnId());
} catch (OrbitException e) { // exception -> result
return new ChargeResult(false, e.getMessage());
}
}
}
// ---- The client: written once, against the interface it wanted. ----
static String checkout(PaymentGateway gateway, String name, String rider, int pence) {
ChargeResult r = gateway.charge(rider, pence);
return "checkout via " + name + ": " + pence + "p " + (r.ok() ? "charged, ref " + r.detail() : "declined (" + r.detail() + ")");
}
public static void main(String[] args) {
Map<String, PaymentGateway> gateways = new LinkedHashMap<>();
gateways.put("lumen", new LumenAdapter(new LumenPaySdk()));
gateways.put("orbit", new OrbitAdapter(new OrbitCardsSdk()));
for (Map.Entry<String, PaymentGateway> e : gateways.entrySet()) System.out.println(checkout(e.getValue(), e.getKey(), "rider-7", 280));
for (Map.Entry<String, PaymentGateway> e : gateways.entrySet()) System.out.println(checkout(e.getValue(), e.getKey(), "rider-7", 99_999));
}
}Output (the same in all three languages)
checkout via lumen: 280p charged, ref LP-1001 checkout via orbit: 280p charged, ref ORB-1 checkout via lumen: 99999p declined (insufficient funds) checkout via orbit: 99999p declined (limit exceeded)
6. Try it
Follow one charge through an adapter
Choose a provider and an amount, and watch the call change shape on the way in and the answer change shape on the way out.
Lumen declines above £500.00; Orbit throws above 50,000 minor units. Checkout sees the same kind of result either way.
7. When not to use it
What you get
- Client code stays put: it keeps the interface it was written for.
- Quirks are contained: one class per foreign API.
- Swappable providers: a new adapter, no edits elsewhere.
- Testable: fake the target, not the SDK.
What you pay
- One more layer: each call passes through a translator.
- Lowest common denominator: features one provider has and the target lacks are hidden.
- Leaky translations: error codes, rounding and timeouts don't always map cleanly.
- A class per adaptee: many small adapters to maintain.
| If… | Use instead |
|---|---|
| You want a simpler front for a whole subsystem | 08 Facade |
| You want to add behaviour, keeping the same interface | 07 Decorator |
| You control both sides | Change one of them to match; no adapter needed |
| You need to control access to an object with the right interface | 09 Proxy |
8. How it goes wrong
charge() returns Orbit's transaction object, checkout depends on Orbit again. Translate the outputs too.9. Remember it and say it
How this hook works: what “A travel plug” means
What the slogan means, phrase by phrase
- “Same hairdryer”
- Checkout, unchanged.
- “foreign socket”
- A provider's SDK with its own interface.
- “only translates”
- Units, names and errors, nothing more.
Why this picture fits: Nobody rewires a hairdryer for a holiday; they carry the cheap thing that fits both.
The story → the concept
| The trip | The code |
|---|---|
| The hairdryer | checkout() |
| The hotel socket | LumenPaySdk |
| The travel adapter | LumenAdapter |
| A second country | OrbitAdapter |
Watch it on an example
- Checkout calls
charge("rider-7", 280). - The Lumen adapter sends
make_payment("2.80", "rider-7"). - It turns
{"status": "ok", "ref": "LP-1001"}into a charged result.
The trap it saves you from
Letting a provider's units and error style leak into every caller.
It still works when…
Yes: wrapping legacy code, third-party SDKs, and old interfaces after a redesign.
In an interview or code review
- Say what you can't change: "We can't change either SDK and don't want to change checkout, so one adapter per provider implements our
PaymentGateway." - Name the translations: units (pence to a pounds string), errors (exceptions to results), names.
- Distinguish it: an adapter changes the interface; a decorator keeps it and adds behaviour; a facade simplifies a whole subsystem.
- Link it to architecture: in hexagonal architecture, these are exactly the adapters that plug into ports.
10. Check yourself
What does an adapter change?
Orbit throws on decline; checkout expects a result. Where is that translated?
Which belongs in an adapter?
Adapter or decorator: logging around a gateway with the same interface?
charge() returns Orbit's own transaction type. What's wrong?
11. Go deeper
| Resource | Why |
|---|---|
| Architecture styles 03: Hexagonal | Ports and adapters: the same idea as an architecture. |
| 08 Facade | The neighbour pattern people confuse it with. |
Python io.TextIOWrapper | A standard-library adapter: bytes stream in, text stream out. |
| Wikipedia: Adapter pattern | Object and class adapters. |