Design Patterns, Explained Simply · Structural Patterns: Fitting Objects Together
Adapter
Make a class with the wrong interface work where your code expects another, by wrapping it in a small translator.
The problem
Your shop's checkout code works with a simple PaymentProvider interface: charge(cents, currency) returns a transaction id. Finance now requires an old in-house billing system for some customers. Its API is different: submit(amount, ccy) takes the amount as a string in dollars ("12.50") and returns a dictionary with a ref field.
You can't change the billing library (another team owns it), and you don't want if billing == "legacy" conversions sprinkled through checkout.
Real-world analogy
A travel plug adapter. Your laptop charger has a US plug; the hotel wall has a UK socket. You don't rewire the hotel or your charger. A small adapter in between translates one shape into the other.
Without the pattern
def checkout(provider, cents):
if isinstance(provider, LegacyBilling): # checkout now knows about every odd API
result = provider.submit(f"{cents // 100}.{cents % 100:02d}", "USD")
return result["ref"]
return provider.charge(cents, "USD")
The pattern
- Target: the interface your code expects (
PaymentProvider.charge). - Adaptee: the existing class with an incompatible interface (
LegacyBilling.submit). - Adapter: implements the target and translates each call to the adaptee: converting units, renaming methods, reshaping results.
- Client: checkout, which only ever sees the target interface.
The code
class CardProvider: # already matches our interface
def charge(self, cents, currency):
return f"card-{cents}-{currency}"
class LegacyBilling: # third-party code we can't change
def submit(self, amount, ccy): # amount is a dollar string like "12.50"
return {"ref": f"LB:{amount}:{ccy}", "status": "OK"}
class LegacyBillingAdapter: # implements charge(), translates to submit()
def __init__(self, legacy):
self.legacy = legacy
def charge(self, cents, currency):
amount = f"{cents // 100}.{cents % 100:02d}" # convert units
result = self.legacy.submit(amount, currency)
return result["ref"] # reshape the result
def checkout(provider, cents): # knows only charge()
return f"paid, transaction {provider.charge(cents, 'USD')}"
print(checkout(CardProvider(), 1250))
print(checkout(LegacyBillingAdapter(LegacyBilling()), 1250))
Output
paid, transaction card-1250-USD paid, transaction LB:12.50:USD
interface PaymentProvider {
charge(cents: number, currency: string): string;
}
class CardProvider implements PaymentProvider { // already matches our interface
charge(cents: number, currency: string) { return `card-${cents}-${currency}`; }
}
class LegacyBilling { // third-party code we can't change
submit(amount: string, ccy: string) { // amount is a dollar string like "12.50"
return { ref: `LB:${amount}:${ccy}`, status: "OK" };
}
}
class LegacyBillingAdapter implements PaymentProvider { // implements charge(), translates to submit()
constructor(private legacy: LegacyBilling) {}
charge(cents: number, currency: string): string {
const amount = `${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, "0")}`; // convert units
return this.legacy.submit(amount, currency).ref; // reshape the result
}
}
function checkout(provider: PaymentProvider, cents: number): string { // knows only charge()
return `paid, transaction ${provider.charge(cents, "USD")}`;
}
console.log(checkout(new CardProvider(), 1250));
console.log(checkout(new LegacyBillingAdapter(new LegacyBilling()), 1250));
Output
paid, transaction card-1250-USD paid, transaction LB:12.50:USD
import java.util.Map;
public class Main {
static String checkout(PaymentProvider provider, int cents) { // knows only charge()
return "paid, transaction " + provider.charge(cents, "USD");
}
public static void main(String[] args) {
System.out.println(checkout(new CardProvider(), 1250));
System.out.println(checkout(new LegacyBillingAdapter(new LegacyBilling()), 1250));
}
}
interface PaymentProvider {
String charge(int cents, String currency);
}
class CardProvider implements PaymentProvider { // already matches our interface
public String charge(int cents, String currency) { return "card-" + cents + "-" + currency; }
}
class LegacyBilling { // third-party code we can't change
Map<String, String> submit(String amount, String ccy) { // amount is a dollar string like "12.50"
return Map.of("ref", "LB:" + amount + ":" + ccy, "status", "OK");
}
}
class LegacyBillingAdapter implements PaymentProvider { // implements charge(), translates to submit()
private final LegacyBilling legacy;
LegacyBillingAdapter(LegacyBilling legacy) { this.legacy = legacy; }
public String charge(int cents, String currency) {
String amount = String.format("%d.%02d", cents / 100, cents % 100); // convert units
return legacy.submit(amount, currency).get("ref"); // reshape the result
}
}
Output
paid, transaction card-1250-USD paid, transaction LB:12.50:USD
Run this on your computer — see “Get Started” for setup.
<?php
interface PaymentProvider {
public function charge(int $cents, string $currency): string;
}
class CardProvider implements PaymentProvider { // already matches our interface
public function charge(int $cents, string $currency): string { return "card-$cents-$currency"; }
}
class LegacyBilling { // third-party code we can't change
public function submit(string $amount, string $ccy): array { // amount is a dollar string like "12.50"
return ["ref" => "LB:$amount:$ccy", "status" => "OK"];
}
}
class LegacyBillingAdapter implements PaymentProvider { // implements charge(), translates to submit()
public function __construct(private LegacyBilling $legacy) {}
public function charge(int $cents, string $currency): string {
$amount = sprintf("%d.%02d", intdiv($cents, 100), $cents % 100); // convert units
return $this->legacy->submit($amount, $currency)["ref"]; // reshape the result
}
}
function checkout(PaymentProvider $provider, int $cents): string { // knows only charge()
return "paid, transaction " . $provider->charge($cents, "USD");
}
echo checkout(new CardProvider(), 1250), "\n";
echo checkout(new LegacyBillingAdapter(new LegacyBilling()), 1250), "\n";
Output
paid, transaction card-1250-USD paid, transaction LB:12.50:USD
Checkout didn't change at all. Everything specific to the legacy API (dollar strings, the ref key) lives in one small class you can test on its own.
When to use it
- You want to use an existing class (third-party SDK, legacy module, another team's service) whose interface doesn't match what your code expects.
- You are wrapping vendors behind your own interface so you can switch providers later (payments, email, storage, maps).
- You want to keep a third-party library's types from spreading through your codebase.
When not to use it
- You own both sides and can simply change one interface to match. Do that instead.
- The "adapter" would have to invent behaviour the adaptee doesn't support. That is a new implementation, not a translation.
- Wrapping everything "just in case" adds a layer to read for no current benefit.
You've already used it
- Java:
Arrays.asList()adapts an array to theListinterface;InputStreamReaderadapts a byte stream to a character reader. - Laravel: the filesystem layer (Flysystem) adapts local disk, S3 and FTP to one
StorageAPI. Its storage drivers are literally called adapters. - Python: DB-API drivers adapt each database's C library to the standard
connect()/cursor()interface.
Key points
- Adapter translates one interface into another, so incompatible classes can work together.
- The client only knows the target interface; all translation lives in the adapter.
- Great for wrapping third-party and legacy code and keeping vendor details out of your domain.
- Further reading: Refactoring Guru: Adapter.
Practice questions
What problem does the Adapter pattern solve?
Answer
It lets a class with an incompatible interface work with code that expects a different interface, by wrapping it in an adapter that translates calls, units and results. The client and the adapted class stay unchanged.
Why is it useful to wrap third-party SDKs behind your own interface?
Answer
It keeps vendor-specific types and quirks out of your business logic, makes the code testable with fakes, and lets you switch or add providers by writing another adapter instead of editing every caller.
Give an example of Adapter in a well-known library.
Answer
Laravel's filesystem (built on Flysystem) adapts local disk, S3, FTP and others to one Storage API. In Java, Arrays.asList() adapts an array to the List interface and InputStreamReader adapts bytes to characters.
Open in the app to mark lessons complete, take notes and track your progress.