Skip to content

Adapter

Making an incompatible interface fit — the boundary that keeps a vendor's vocabulary out of your domain, and the anti-corruption layer it grows into.

Beginner17 minUpdated 8 August 2026

Adapter is the pattern with the highest ratio of usefulness to complexity. It has one idea — a small object that translates between an interface you want and an interface you have — and it is the single most reliable investment you can make at the edge of a system.

The problem is not that the third-party library is bad. It is that its vocabulary is not yours, and every place your code speaks its vocabulary is a place that changes when the vendor does.

The shape

// Your code wants to say this
gateway.charge({ amountInCents: 2999, currency: 'INR', idempotencyKey: key });

// The vendor's SDK says this
sdk.transactions.create({ value: 29.99, iso: 'INR', ref: key, capture: true });

Without an adapter, the conversion — cents to units, currency to iso, the capture flag nobody remembers the meaning of — is repeated at every call site, and each repetition is a chance to get the decimal point wrong.

class VendorPaymentGateway {
  constructor(sdk) { this.sdk = sdk; }

  async charge({ amountInCents, currency, idempotencyKey }) {
    const res = await this.sdk.transactions.create({
      value: amountInCents / 100,
      iso: currency,
      ref: idempotencyKey,
      capture: true,
    });
    if (res.status === 'declined') throw new PaymentDeclined(res.reason);
    return new Receipt(res.id, res.value * 100, res.iso);
  }
}
classDiagram
    class PaymentGateway {
        <<interface>>
        +charge(request) Receipt
    }
    class VendorPaymentGateway {
        -sdk: VendorSdk
        +charge(request) Receipt
    }
    class VendorSdk {
        +transactions.create(payload)
    }
    class Checkout
    PaymentGateway <|.. VendorPaymentGateway
    VendorPaymentGateway --> VendorSdk
    Checkout --> PaymentGateway

What the boundary is actually worth

Replacing the vendor touches one file. This is the benefit everyone names and the one least often collected — most systems change payment provider rarely. The other three are collected every week.

The vendor's failures become your failures. res.status === 'declined' is a vendor concept; PaymentDeclined is yours. Without the adapter, every caller must know that a declined payment arrives as a successful HTTP response with a status field — a rule the vendor may change in a minor release.

Units and types are converted once. Money in cents everywhere inside; the conversion to and from the vendor's decimal happens at exactly one boundary. Currency bugs live in adapters that do not exist.

Testing stops needing the network. A fake implementing your interface is three lines. Faking the vendor's SDK means reproducing its shape, its error semantics and its quirks.

Adapter versus its neighbours

Four structural patterns wrap one object in another, and telling them apart is a matter of intent, not shape.

PatternIntent
AdapterChange an interface into a different one you already require
FacadeSimplify a complicated interface into an easier one
DecoratorKeep the same interface and add behaviour
ProxyKeep the same interface and control access to the subject

The distinguishing question for Adapter: did the target interface exist before you wrote the wrapper? If your code already spoke PaymentGateway and the vendor did not fit, that is Adapter. If you invented a simpler surface because the original was unpleasant, that is Facade.

When it grows into an anti-corruption layer

A thin adapter converts names and units. When the external system's model disagrees with yours — not just its spelling — the boundary has to do more.

A logistics API that calls everything a "shipment", including a return, a transfer between warehouses and a customer delivery, has a model that will corrupt yours if you let it in. The boundary then translates concepts rather than fields: incoming shipments become Delivery, Return or StockTransfer based on rules you own.

This is the anti-corruption layer from domain-driven design, and it is Adapter with a bigger remit. The signal that you need one is a mapping method that has to make a judgement rather than rename a field.

The one that goes the other way

Adapters are usually written to make something external fit inwards, but the same pattern makes your own code fit an external contract: a class that implements a framework's expected interface and delegates to your domain service. That is still Adapter, and keeping it thin matters even more — it is the file that will be rewritten when the framework version changes.

Check your understanding

Common mistakes

Shaping the target interface around the vendor. Then nothing is adapted, and the vendor's vocabulary is now yours by another name.

Letting business rules settle in the adapter. They become untestable without the vendor's shape and invisible from the domain.

Leaking vendor types through. Returning the SDK's response object means every caller depends on it, and the boundary has achieved nothing.

One adapter per vendor method. An adapter is defined by the interface it satisfies, not by mechanical one-to-one forwarding.

Not translating errors. If a caller catches a vendor exception, the vendor's failure taxonomy is part of your domain.

Adapting something you also control. If both sides are yours, change one of them instead of adding a translator forever.

Practice

A team is integrating a shipping provider whose API models returns, warehouse transfers and customer deliveries all as a 'shipment' with a 20-field payload and a `type` code. Design the boundary, and say how you would decide between a thin adapter and a full anti-corruption layer.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Ask whether the mapping renames fields or makes judgements.

Show a strong answer

The deciding question is whether mapping requires a judgement. Renaming recipient_addr to destination is translation and belongs in a thin adapter. Deciding that type: 'RTN' with a non-null origin_warehouse is a StockTransfer rather than a Return is a judgement — it encodes knowledge about our model that the provider does not have. Once one such rule exists there will be five, and they need somewhere to live that is not scattered through mapping code.

Here it is clearly the second case, so I would build an anti-corruption layer:

  • Define our own types first: Delivery, Return, StockTransfer. They carry only the fields our domain uses, which will be far fewer than twenty — most of the payload is provider bookkeeping.
  • One translation module owns the judgement, with the classification rules in one readable function. It should be possible to read that function and learn how the two models differ, which is documentation nothing else provides.
  • Nothing above the boundary sees a "shipment". If the word appears in a domain service, the layer has leaked.
  • Test the translation against recorded real payloads, including the ones that motivated each rule. These tests are the specification of the mismatch, and they are what catches a provider changing a type code.

How I would decide in a less clear case: start thin, and treat the first judgement as the trigger. A thin adapter that grows a second if on a field's meaning has told you it wants to be a layer. Reaching for the heavier structure before that happens usually produces a translation layer that translates nothing.

What I would watch for as it grows:

  • Fee, timing or eligibility rules appearing in the layer. Those are domain policy that has drifted in because the layer is where the data first arrives. They belong above it.
  • Our types acquiring provider-shaped optional fields. A Delivery with providerStatusCode is the provider's model wearing our name, and it means the mapping gave up on a case rather than deciding it.

You inherit a codebase where the payment SDK's client is imported in 30 files. Design the retrofit, and explain how you would keep it from stalling halfway.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The end state is easy to describe. The risk is a partial migration that leaves two ways of doing everything.

Show a strong answer

Start by writing the interface from the call sites, not from the SDK. Read all 30 usages and list what the application actually asks the vendor to do. It will almost always be a handful of operations — charge, refund, look up — hidden behind many variations of the same call. That list is the interface, and deriving it this way avoids the trap of mirroring the SDK.

Then the mechanical work:

  • Implement the adapter and route the easiest call sites through it first, to establish the shape and get the review argument settled while the change is small.
  • Convert one file per change, each independently correct and revertible. A single 30-file change cannot be reviewed and cannot be bisected.
  • Translate errors in the adapter from the start. If callers still catch vendor exceptions, the migration will look finished while the coupling remains.

Keeping it from stalling — this is the part that actually decides the outcome:

  • A lint rule banning new imports of the SDK outside the adapter, added as soon as the adapter exists. Without it the migration races against new feature code and never converges. Grandfather the existing 30.
  • Track the number of remaining direct importers as one visible number. A migration without a number is a migration nobody can tell has stalled.
  • Convert files you are already touching for other reasons, rather than scheduling dedicated migration time that will lose to feature work.
  • Do the hardest file early, not last. There is usually one call site doing something strange — a streaming response, a webhook verification — and if it is left until the end it becomes the reason the adapter grows an escape hatch. Finding out early whether the interface can accommodate it is worth more than the easy wins.

How I would know it worked: the SDK appears in one file; tests of payment-related code construct a fake gateway rather than stubbing HTTP; and a version bump of the SDK is a diff a reviewer can read in one screen.

What I would not promise: that this makes changing provider easy. It makes it possible to scope, which is a smaller and more honest claim. The everyday wins — one place for money conversion, one error taxonomy, tests without a network — are what justifies the work.

Key takeaways

  • Adapter translates between an interface you require and an interface you have. Write the one you want first.
  • The benefits collected daily are one place for unit conversion, one error taxonomy, and tests without a network — not the rare vendor swap.
  • Never leak vendor types or vendor exceptions through the boundary, or the boundary has achieved nothing.
  • Distinguish it from neighbours by intent: Facade simplifies, Decorator adds behaviour at the same interface, Proxy controls access.
  • When mapping requires a judgement rather than a rename, the adapter has become an anti-corruption layer and should be structured as one.
  • Business rules do not belong in an adapter, however convenient it is that the adapter understands both worlds.
  • Retrofit incrementally, and add a lint rule banning new direct imports on day one or the migration will never converge.

Further reading

  • Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
  • Eric Evans, Domain-Driven Design (Addison-Wesley, 2003) — the anti-corruption layer.
  • Alistair Cockburn, "Hexagonal Architecture" (2005) — ports and adapters as an architectural stance.
  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — wrapping third-party code to create a testing seam.
  • Steve Freeman and Nat Pryce, Growing Object-Oriented Software, Guided by Tests (Addison-Wesley, 2009) — on not mocking types you do not own.

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonDecorator
Lessons in this track