Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 24

    Adapter Pattern

    Adapter (Wrapper) bridges two incompatible interfaces.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Adapter (Wrapper) bridges two incompatible interfaces. Your domain expects interface A; the only available code exposes B. An Adapter implements A in terms of B — translating method names, argument order, error semantics, and data shapes. Every SDK wrapper, legacy integration, and vendor port behind a domain interface is an Adapter.

    The story

    A retailer migrated payment from a 12-year-old SOAP processor to REST. Rewriting 80 call sites would take months. The team built PaymentGateway, wrapped SOAP in LegacySoapAdapter, swapped one line at the composition root, and shipped StripeAdapter in parallel — domain code never noticed.

    The business problem

    Direct vendor coupling blocks swaps, tests, and clean domain models:

    • Vendor lock-in: domain code speaks Stripe/Adyen/SOAP dialects everywhere.
    • Swap cost: changing provider means editing dozens of call sites.
    • Untestable integrations: tests hit real APIs or skip coverage.
    • Error chaos: vendor exceptions leak into business logic.

    The problem teams faced

    Adapter fits when:

    • Third-party or legacy code has the capability you need but the wrong interface.
    • You want a domain port without rewriting every consumer.
    • You expect to swap implementations (SOAP → REST, vendor A → B).
    • Tests need a fake seam without forking the vendor library.

    Understanding the topic

    Intent: Convert the interface of a class into another interface clients expect.

    • Target — domain interface the client expects.
    • Adaptee — existing class with incompatible interface.
    • Adapter — implements Target, delegates to Adaptee with translation.
    • Object adapter — composition (preferred); class adapter uses multiple inheritance.

    Internal architecture

    Object adapter (composition):

    ts
    interface PaymentGateway { charge(amount: Money): Promise<Receipt> }
    class StripeAdapter implements PaymentGateway {
    constructor(private stripe: StripeClient) {}
    async charge(amount: Money): Promise<Receipt> {
    try {
    const intent = await this.stripe.paymentIntents.create({
    amount: amount.cents,
    currency: amount.currency,
    })
    return { id: intent.id, status: mapStatus(intent.status) }
    } catch (e) {
    throw new PaymentFailedError(mapStripeError(e)) // never leak Stripe types
    }
    }
    }

    Visual explanation

    Three diagrams cover Adapter flow, vs Facade/Decorator, and wiring:

    Adapter translation flow
    Client
    Uses Target
    Adapter
    Implements Target
    Translate
    Args · errors · shapes
    Adaptee
    Vendor / legacy
    All translation lives in the Adapter — consumers never see Adaptee types.
    Adapter vs Facade vs Decorator
    One class, wrong IF?
    Adapter
    Many classes, too complex?
    Facade
    Same IF, add behaviour?
    Decorator
    Same IF, control access?
    Proxy
    Adapter changes interface; Facade simplifies many; Decorator adds behaviour.
    Wire at composition root
    Define Target port
    Domain language
    Implement Adapter
    One per vendor
    DI binds Target
    Prod vs test swap
    Consumers unchanged
    Depend on port
    Swap vendor by changing one binding — not 80 call sites.

    Informative example

    Before / after — direct vendor coupling to Adapter port:

    ts
    // ❌ Domain speaks Stripe everywhere — swap = rewrite call sites
    async function checkout(cart: Cart) {
    const intent = await stripe.paymentIntents.create({
    amount: cart.total.cents,
    currency: "usd",
    })
    if (intent.status === "requires_action") throw new Error(intent.last_payment_error?.message)
    }
    // ✅ Adapter isolates vendor; domain speaks PaymentGateway
    interface PaymentGateway {
    charge(cart: Cart): Promise<Receipt>
    }
    class StripeAdapter implements PaymentGateway {
    constructor(private client: StripeClient) {}
    async charge(cart: Cart): Promise<Receipt> {
    const intent = await this.client.paymentIntents.create({ /* ... */ })
    return mapToReceipt(intent) // domain types only
    }
    }
    class CheckoutService {
    constructor(private payments: PaymentGateway) {}
    checkout(cart: Cart) { return this.payments.charge(cart) }
    }

    Execution workflow

    1Adapter implementation workflow
    1 / 5

    Define Target

    Domain port in domain language — not vendor vocabulary.

    PaymentGateway, not StripeClient.

    Real-world use

    Arrays.asList (array → List), InputStreamReader (bytes → chars), SLF4J bindings, Spring HandlerInterceptorAdapter, React wrappers for jQuery widgets, multi-cloud storage abstractions (S3/GCS/Azure under one interface).

    Production case study

    Bank mainframe SOAP → GraphQL migration:

    • Scenario: 1998 mainframe via SOAP; web tier moving to GraphQL.
    • Challenge: CAPS_SNAKE_CASE fields, sentinel error codes, EBCDIC strings.
    • Decision: InventoryAdapter implementing InventoryPort; hand-translate fields and lift errors.
    • Outcome: GraphQL resolvers stayed clean; mainframe decommissioned years later by swapping one adapter.

    Trade-offs

    • Pro: decouples domain from vendor; swap at composition root.
    • Pro: translation logic in one testable place.
    • Con: extra layer to maintain; DTO mapping overhead on large payloads.
    • Con: leaky adapters (passing vendor types through) defeat the purpose.

    Decision framework

    • Use when depending on third-party code with wrong interface.
    • Use when you expect to swap implementations.
    • Avoid when you control both sides — refactor interfaces to match.
    • Avoid when adapter only renames one method — that's noise.

    Best practices

    • Object adapter (composition) by default — testable, no inheritance lock-in.
    • Normalize all errors inside adapter — consumers never catch vendor exceptions.
    • Contract tests shared across every adapter implementation.
    • Map every input and output — no raw vendor types on Target interface.

    Anti-patterns to avoid

    • Leaky adapter passing StripeError through domain interface.
    • Adapter that wraps a whole subsystem — that's a Facade.
    • Adapter "for consistency" when you control both interfaces.

    Common mistakes

    • Partial error mapping — one vendor exception leaks and spreads.
    • Adapter doing business logic — belongs in domain, not translation layer.
    • One mega-adapter for unrelated vendor APIs — split by port.

    Debugging tips

    • Diff which adapter implementation composition root wired when envs diverge.
    • Log translation boundaries — correlate IDs across adapter in/out.
    • Contract test failure pinpoints which adapter broke on vendor API change.

    Optimization strategies

    • Batch outbound calls at adapter layer — avoid N+1 through port.
    • Cache read-heavy adapter mappings when vendor schema is stable.

    Common misconceptions

    • Adapter ≠ Facade. Adapter translates one interface; Facade simplifies many.
    • Adapter ≠ Decorator. Decorator keeps same interface and adds behaviour.
    • Bridge is designed up-front; Adapter is typically retrofit integration.

    Advanced interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    4 questions
    1IntermediateQuestionAdapter, Facade, Decorator, Proxy — one sentence each?+

    Answer

    Adapter: changes interface. Facade: simplifies subsystem. Decorator: adds behaviour, same interface. Proxy: controls access, same interface.

    Follow-up

    Which two confuse most?
    2AdvancedQuestionWhy object adapter over class adapter?+

    Answer

    Composition works in single-inheritance languages, adapts subclasses of adaptee, easier to test via injection.

    Follow-up

    When class adapter appears?
    3IntermediateQuestionWhat is a leaky adapter?+

    Answer

    Passes raw adaptee types or exceptions through Target. Consumers depend on vendor anyway.

    Follow-up

    Vendor type you've seen leak?
    4IntermediateQuestionWhere put error translation?+

    Answer

    Inside adapter exhaustively. Target specifies domain exceptions; adapter catches vendor ones and re-throws.

    Follow-up

    Testing strategy?

    Summary

    You can design domain ports, implement object adapters with full error translation, and swap vendors at the composition root. Tell the SOAP→REST payment story — if you can do that, you own Adapter.

    Ready to mark this lesson complete?Track your journey across the entire course.