Adapter Pattern
Adapter (Wrapper) bridges two incompatible interfaces.
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):
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:
Informative example
Before / after — direct vendor coupling to Adapter port:
// ❌ Domain speaks Stripe everywhere — swap = rewrite call sitesasync 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 PaymentGatewayinterface 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
Define Target
Domain port in domain language — not vendor vocabulary.
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.
1IntermediateQuestionAdapter, Facade, Decorator, Proxy — one sentence each?+
Answer
Follow-up
2AdvancedQuestionWhy object adapter over class adapter?+
Answer
Follow-up
3IntermediateQuestionWhat is a leaky adapter?+
Answer
Follow-up
4IntermediateQuestionWhere put error translation?+
Answer
Follow-up
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.