Skip to content
aipatterns.dev

Design patternStructural Patterns

Adapter

Wrap an incompatible class in an object that exposes the interface your code expects, so you can use it without changing either side.

Complexity
Low
Seen in the wild
High
Also known as
Wrapper, Translator

Intent

Make a class with the wrong interface usable by code that expects a different one. The adapter implements the interface your code owns and translates each call into the calls, argument shapes and error conventions of the class it wraps. Neither the caller nor the wrapped class changes.

Forces

  • Your domain's vocabulary vs a vendor SDK's vocabulary: your code says send(phone, message); the SDK wants a payload object, a stripped phone number and a sender ID.
  • Code you own vs code you cannot edit: third-party libraries, generated clients and legacy modules are fixed, but your callers should not bend to them.
  • One integration point vs SDK calls scattered everywhere: when every caller talks to the vendor directly, switching vendors becomes Shotgun Surgery.
  • Error conventions: the SDK returns status codes, your code expects exceptions or error values. Someone has to translate, exactly once.

Therefore

Define the interface your code wants, then write one small class that implements it by delegating to the incompatible class and translating inputs, outputs and errors.

The interface belongs to your side. In hexagonal architecture it is the port, and this class is the adapter that plugs a vendor into it. Callers depend only on that interface, so tests use a fake, and a vendor switch means writing a second adapter. Keep the adapter thin: translation only, no business rules.

Structure

Diagram source (Mermaid)
classDiagram
  class SignupService {
    -SmsSender sms
    +sendLoginCode(phone, code) void
  }
  class SmsSender {
    <<interface>>
    +send(phone, message) void
  }
  class TelcoSmsAdapter {
    -TelcoClient client
    -String senderId
    +send(phone, message) void
  }
  class TelcoClient {
    +dispatch(payload) VendorResult
  }
  SignupService --> SmsSender
  SmsSender <|.. TelcoSmsAdapter
  TelcoSmsAdapter --> TelcoClient : delegates

Participants

  • Target (SmsSender): the interface your code depends on, shaped by your needs, not the vendor's.
  • Adaptee (TelcoClient): the existing class with the incompatible interface. You do not modify it.
  • Adapter (TelcoSmsAdapter): implements the target, holds the adaptee, and translates in both directions.
  • Client (SignupService): calls the target and never sees the adaptee's types.

Use it when

  • You integrate a third-party SDK, generated API client or legacy module whose method names, argument types or error style don't match your code.
  • You want to swap a vendor later, or run two vendors side by side, behind one interface.
  • Vendor types are leaking into domain code: imports of the SDK appear in services, handlers and tests.
  • You need a fake for tests and the real dependency has no interface to implement.
  • Two existing classes do the same job with different method signatures (Alternative Classes with Different Interfaces) and you can't rename one of them.

Do not use it when

  • You own both sides. Change the class's interface directly (Rename Method, Add Parameter) instead of wrapping your own code.
  • The mismatch is one function call in one place. A local helper function that converts arguments is enough; a class and an interface for a single call site is ceremony.
  • The only difference is a function's shape and your language has first-class functions. Pass a lambda such as (phone, msg) => client.dispatch({...}) where a one-method interface is expected.
  • You are about to add caching, retries or logging. That is Decorator: same interface in and out, extra behavior. An adapter changes the interface and adds nothing.
  • You want to hide a whole subsystem of several classes behind a simpler entry point. That is Facade.

Application grill

Inspect first: both interfaces and who owns each side (your code versus a third-party SDK with its own version cadence); every call site that would bypass the adapter and hit the vendor directly; the SDK version pinned in the lockfile.

Missing pieces: none beyond the language: one class or function that exposes your interface and delegates. The adapter is the one file to touch on an SDK upgrade - say so in its doc comment so upgrades stay local.

Ask before you build:

  • Scale: is the wrapped call on a hot path where per-call overhead or connection reuse (pooling inside the SDK) matters?
  • Performance: does the adapter own one long-lived client, or build a new client per call?
  • Reliability: how are vendor errors translated - an error map or a rethrow - and which side's contract do callers now see?
  • Budget: one adapter for one consumer, or an interface several services will share?
  • Implementation: can tests run the adapter against a fake vendor, and are the error translations covered?

Implementation steps

  1. Write the target interface from the caller's point of view: only the methods callers need, using your domain types.
  2. Create the adapter class implementing the target. Take the adaptee as a constructor argument (do not construct it inside).
  3. Implement each method by converting arguments, calling the adaptee and converting the result.
  4. Map the adaptee's errors to your own error types, so callers never catch vendor exceptions or check vendor codes.
  5. Replace direct adaptee calls in client code with calls through the target interface, one call site at a time, running tests after each.
  6. Wire the adapter at the composition root and give tests a fake implementation of the target.

Example

A signup service sends login codes by SMS. The telecom SDK wants a payload with a digits-only msisdn and reports failure with numeric codes instead of errors.

// Target: the interface your code owns.
interface SmsSender {
  send(phone: string, message: string): void;
}

// --- Third-party SDK (simulated). You cannot edit this code. ---
interface VendorPayload { msisdn: string; text: string; senderId: string }
interface VendorResult { code: number; detail: string }

class TelcoClient {
  dispatch(p: VendorPayload): VendorResult {
    if (!/^\d{8,15}$/.test(p.msisdn)) return { code: 21, detail: "invalid msisdn" };
    console.log(`[telco] ${p.senderId} -> ${p.msisdn}: ${p.text}`);
    return { code: 0, detail: "queued" };
  }
}
// --- end SDK ---

class SmsDeliveryError extends Error {}

class TelcoSmsAdapter implements SmsSender {
  constructor(
    private readonly client: TelcoClient,
    private readonly senderId: string,
  ) {}

  send(phone: string, message: string): void {
    const result = this.client.dispatch({
      msisdn: phone.replace(/^\+/, ""),
      text: message,
      senderId: this.senderId,
    });
    if (result.code !== 0) {
      throw new SmsDeliveryError(`telco error ${result.code}: ${result.detail}`);
    }
  }
}

// Client code: knows SmsSender, never TelcoClient.
function sendLoginCode(sms: SmsSender, phone: string, code: string): void {
  sms.send(phone, `Your login code is ${code}`);
}

const sms: SmsSender = new TelcoSmsAdapter(new TelcoClient(), "ACME");
sendLoginCode(sms, "+4915112345678", "482913");
try {
  sendLoginCode(sms, "+12", "000");
} catch (e) {
  console.log(e instanceof SmsDeliveryError, (e as Error).message); // true telco error 21: invalid msisdn
}

Trade-offs

Gains

  • Vendor types and quirks live in one file. Callers speak your domain's language.
  • Switching or adding a vendor means writing another adapter; client code does not change.
  • Tests swap in a fake SmsSender with no SDK, network or credentials.
  • Error translation happens once, consistently.

Costs

  • One more type and one more hop when reading the call path.
  • A target interface designed too close to the first vendor will not fit the second; you end up adapting the adapter.
  • Features the target does not expose (delivery receipts, unicode options) are hidden until you widen the interface.
  • Adapters attract business logic over time. Keep them translation-only.

Relations

  • Bridge is designed up front so two hierarchies can vary independently. Adapter is applied after the fact to make existing parts fit.
  • Decorator keeps the interface and adds behavior. Adapter changes the interface and adds none.
  • Facade defines a new, simpler interface over many classes. Adapter makes one class fit an interface that already exists.
  • Proxy has the same interface as its subject and controls access to it. Adapter has a different interface from what it wraps.

Agent checklist

  • The target interface is defined in your code's terms and contains only methods callers use.
  • You checked that you cannot change the adaptee itself; if you own it, rename or reshape it instead.
  • The adaptee is injected into the adapter, not constructed inside it.
  • Vendor errors and status codes are translated to your error types; no vendor exception escapes the adapter.
  • No imports of the adaptee's package remain outside the adapter and the composition root.
  • The adapter contains no business rules, and a fake of the target is used in client tests.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.