On this page
Description
Adapter is the structural pattern for making an existing class fit the interface your code expects - like a travel plug between your laptop and a foreign socket. It's how real systems integrate vendor SDKs, legacy modules and third-party libraries without letting their quirks spread through the codebase. This skill covers the pattern, how to apply it, and what an adapter must translate besides method names.
Your code has settled on how it wants to pay for things - but the billing library you have to use speaks a different language: other method names, amounts in cents instead of dollars, numeric status codes instead of exceptions.
The tempting fix is to convert inline, wherever you need it:
Java
class CheckoutService {
void pay(double dollars) {
new LegacyBillingClient().charge((int) Math.round(dollars * 100)); // dollars -> cents, here...
}
}
class RefundService {
void refund(double dollars) {
new LegacyBillingClient().credit((int) Math.round(dollars * 100)); // ...and copy-pasted here
}
}
Python
class CheckoutService:
def pay(self, dollars: float) -> None:
LegacyBillingClient().charge(round(dollars * 100)) # dollars -> cents, here...
class RefundService:
def refund(self, dollars: float) -> None:
LegacyBillingClient().credit(round(dollars * 100)) # ...and copy-pasted here
Every quirk of the library is now scattered across your codebase. When the library changes (or you switch vendors), you have to hunt down every call site.
We'll go from simple to more technical:
- The pattern: target, adaptee and adapter.
- Writing an adapter, step by step.
- Where you'll see adapters in the standard libraries and in integrations.
- What adapters must translate - units, results and errors - and choosing among the structural patterns.
The pattern
An Adapter is a small class that implements the interface your code wants and translates each call into the interface the library actually has.
- Target - the interface your code depends on (
PaymentProcessor). - Adaptee - the existing class with the "wrong" interface (
LegacyBillingClient). You don't change it. - Adapter - implements the target and holds a reference to the adaptee.
Java
interface PaymentProcessor {
void pay(double dollars);
void refund(double dollars);
}
class LegacyBillingAdapter implements PaymentProcessor {
private final LegacyBillingClient client;
LegacyBillingAdapter(LegacyBillingClient client) { this.client = client; }
public void pay(double dollars) { client.charge(toCents(dollars)); }
public void refund(double dollars) { client.credit(toCents(dollars)); }
private int toCents(double dollars) { return (int) Math.round(dollars * 100); }
}
Python
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, dollars: float) -> None: ...
@abstractmethod
def refund(self, dollars: float) -> None: ...
class LegacyBillingAdapter(PaymentProcessor):
def __init__(self, client: LegacyBillingClient):
self._client = client
def pay(self, dollars: float) -> None:
self._client.charge(self._to_cents(dollars))
def refund(self, dollars: float) -> None:
self._client.credit(self._to_cents(dollars))
@staticmethod
def _to_cents(dollars: float) -> int:
return round(dollars * 100)
Now CheckoutService and RefundService just use a PaymentProcessor. The unit conversion lives in exactly one place.
How it works, step by step
- Describe what you need in your own terms: the target interface.
- Write an adapter class that implements that interface.
- Give the adapter the adaptee through its constructor (composition).
- In each method, translate: names, parameter order, units, return values and errors.
- Everything else in your code depends only on the target interface.
Where you'll see it
InputStreamReaderadapts a byteInputStreaminto a characterReader.Arrays.asList(array)lets an array be used wherever aListis expected.- In Python,
io.TextIOWrapperadapts a binary stream into a text stream -InputStreamReader's twin. - Payment, email and SMS integrations are usually hidden behind your own
PaymentGateway/EmailSenderinterface, with one adapter per vendor - switching vendors becomes a one-class change.
Choosing among the structural patterns
| If you need to... | Reach for | What it changes |
|---|---|---|
| Make an existing class fit the interface your code expects | Adapter (this lesson) | Changes the interface; the behavior stays the same. |
| Add behavior to an object without changing its class or interface | Decorator | Same interface, extra behavior, and wrappers can be stacked. |
| Give a complicated subsystem one simple front door | Facade | A new, simpler interface over many classes. |
| Treat a single item and a group of items the same way | Composite | A tree: groups contain items or other groups. |
| Control access to an object (lazy loading, caching, security, remote calls) | Proxy | Same interface, but it decides when and whether the real object is used. |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Vendor exceptions or data classes in your interface | every caller is still coupled to the vendor | translate errors and data into your own types |
| Converting units inline at every call site | quirks scattered; one site gets it wrong | convert once, inside the adapter |
Money through double ((int) (19.99 * 100) is 1998) | off-by-a-cent charges | integer cents or decimal types, explicit rounding |
| Business rules inside the adapter | a hidden service no one reviews as such | keep adapters to translation |
| Adapting an interface your own team owns | a permanent wrapper around a fixable mistake | fix the interface |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Besides method names, what does an adapter have to translate?
Show answer
Units (dollars to cents), return values (a vendor's status codes to your own result type) and errors (vendor exceptions or codes to your own contract) - so no vendor type leaks through your interface.
2. Why is an object adapter (the adaptee in a field) usually preferred to a class adapter (extending the adaptee) in Java?
Show answer
A class can extend only one class, inheritance exposes all of the adaptee's public methods through the adapter, and a field accepts any subclass or test double of the adaptee.
3. The awkward interface belongs to your own team. Should you write an adapter for it?
Show answer
No - fix the interface. Adapters are for code you can't or shouldn't change: third-party libraries, vendor SDKs, frozen legacy modules.
Key takeaways
- An adapter makes an existing class fit the interface your code expects - neither side has to change.
- Translate everything at the boundary: names, units, return values and errors.
- Never let vendor types (exceptions, DTOs) leak through your interface.
- Prefer composition (an object adapter) over inheritance.
- If you own the "wrong" interface, fix it rather than adapting around it.