Design Patterns: Structural · Structural Patterns

Adapter Pattern

Translating one interface into another that calling code already expects.

  • 5 min read
  • 12 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The pattern
  3. How it works, step by step
  4. Where you'll see it
  5. Choosing among the structural patterns
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

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:

  1. The pattern: target, adaptee and adapter.
  2. Writing an adapter, step by step.
  3. Where you'll see adapters in the standard libraries and in integrations.
  4. 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

  1. Describe what you need in your own terms: the target interface.
  2. Write an adapter class that implements that interface.
  3. Give the adapter the adaptee through its constructor (composition).
  4. In each method, translate: names, parameter order, units, return values and errors.
  5. Everything else in your code depends only on the target interface.

Where you'll see it

  • InputStreamReader adapts a byte InputStream into a character Reader.
  • Arrays.asList(array) lets an array be used wherever a List is expected.
  • In Python, io.TextIOWrapper adapts a binary stream into a text stream - InputStreamReader's twin.
  • Payment, email and SMS integrations are usually hidden behind your own PaymentGateway / EmailSender interface, with one adapter per vendor - switching vendors becomes a one-class change.

Choosing among the structural patterns

If you need to...Reach forWhat it changes
Make an existing class fit the interface your code expectsAdapter (this lesson)Changes the interface; the behavior stays the same.
Add behavior to an object without changing its class or interfaceDecoratorSame interface, extra behavior, and wrappers can be stacked.
Give a complicated subsystem one simple front doorFacadeA new, simpler interface over many classes.
Treat a single item and a group of items the same wayCompositeA tree: groups contain items or other groups.
Control access to an object (lazy loading, caching, security, remote calls)ProxySame interface, but it decides when and whether the real object is used.

Common mistakes

MistakeWhat happensDo this instead
Vendor exceptions or data classes in your interfaceevery caller is still coupled to the vendortranslate errors and data into your own types
Converting units inline at every call sitequirks scattered; one site gets it wrongconvert once, inside the adapter
Money through double ((int) (19.99 * 100) is 1998)off-by-a-cent chargesinteger cents or decimal types, explicit rounding
Business rules inside the adaptera hidden service no one reviews as suchkeep adapters to translation
Adapting an interface your own team ownsa permanent wrapper around a fixable mistakefix 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.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You need to...Reach for
Make an existing class fit your interfaceAdapter
Add behavior, same interfaceDecorator
One simple entry point to a subsystemFacade
Treat items and groups alikeComposite
Control access to an objectProxy
Keep another system's whole model out of yoursan anti-corruption layer (adapters at system scale)

Snippets worth knowing by heart

Java

final class StripeGateway implements PaymentGateway {
    private final StripeClient client;
    StripeGateway(StripeClient client) { this.client = client; }

    public ChargeResult charge(long cents, String idempotencyKey) {
        try {
            return map(client.charge(cents, idempotencyKey).status());
        } catch (CardException e) {
            return ChargeResult.DECLINED;
        } catch (TimeoutException e) {
            return ChargeResult.UNKNOWN;          // never guess "declined"
        }
    }
}

Python

class StripeGateway(PaymentGateway):
    def __init__(self, client: StripeClient):
        self._client = client

    def charge(self, cents: int, idempotency_key: str) -> ChargeResult:
        try:
            return self._map(self._client.charge(cents, idempotency_key).status)
        except CardError:
            return ChargeResult.DECLINED
        except TimeoutError:
            return ChargeResult.UNKNOWN           # never guess "declined"

The traps

  • Vendor types in your interface - exceptions, DTOs.
  • Inline conversions at call sites.
  • Money as double.
  • Business rules in the adapter.

Staff-level points, one line each

  • Prefer object adapters: one superclass, no exposed adaptee API, any double fits.
  • (int) (19.99 * 100) and int(19.99 * 100) are 1998 - use integer cents or decimals.
  • Seconds read as milliseconds put a 2025 timestamp in January 1970.
  • New vendor enum values crash strict mappings - tolerant reader: UNKNOWN, log, metric.
  • Timeouts are unknown outcomes: idempotency keys plus reconciliation.
  • Callback to future: future::complete; in asyncio, call_soon_threadsafe(fut.set_result, ...).
  • Pages to streams lazily: 3 items needed only 2 page fetches.
  • Anti-corruption layer = adapters at system scale; keep them honest with contract tests and recorded responses.

Say it in an interview

  • "The rest of the code only knows PaymentGateway; Stripe lives in one adapter."
  • "Amounts cross the boundary as integer cents, with explicit rounding."
  • "An unknown vendor status maps to UNKNOWN and raises an alert instead of failing the request."
  • "A timeout is reported as unknown, and reconciliation settles it later."

Practice

This skill comes with 12 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Adapter Pattern free

Hide Two Payment SDKs Behind One Interface

MediumProCoding problemJava · Python

Your checkout depends on one interface: PaymentGateway.charge(cents), which must return exactly **APPROVED, DECLINED or INVALID**. Two vendor SDKs speak very different languages:

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Adapter Pattern free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime