# Adapter

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

- kind: pattern
- category: structural
- aliases: Wrapper, Translator
- canonical: https://aipatterns.dev/design-patterns/adapter
- inspired-by: https://refactoring.guru/design-patterns/adapter

## 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](https://aipatterns.dev/refactoring/smells/shotgun-surgery.md).
- **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

```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](https://aipatterns.dev/refactoring/smells/alternative-classes-with-different-interfaces.md)) 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](https://aipatterns.dev/refactoring/techniques/rename-method.md), [Add Parameter](https://aipatterns.dev/refactoring/techniques/add-parameter.md)) 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](https://aipatterns.dev/design-patterns/decorator.md): 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](https://aipatterns.dev/design-patterns/facade.md).

## 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.

```ts title="notifications.ts"
// 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
}
```

```python title="notifications.py"
from dataclasses import dataclass
from typing import Protocol

class SmsSender(Protocol):
    def send(self, phone: str, message: str) -> None: ...

# --- Third-party SDK (simulated). You cannot edit this code. ---
@dataclass
class VendorResult:
    code: int
    detail: str

class TelcoClient:
    def dispatch(self, payload: dict) -> VendorResult:
        msisdn = payload["msisdn"]
        if not (msisdn.isdigit() and 8 <= len(msisdn) <= 15):
            return VendorResult(21, "invalid msisdn")
        print(f"[telco] {payload['sender_id']} -> {msisdn}: {payload['text']}")
        return VendorResult(0, "queued")
# --- end SDK ---

class SmsDeliveryError(Exception):
    pass

class TelcoSmsAdapter:
    def __init__(self, client: TelcoClient, sender_id: str) -> None:
        self._client = client
        self._sender_id = sender_id

    def send(self, phone: str, message: str) -> None:
        result = self._client.dispatch(
            {"msisdn": phone.removeprefix("+"), "text": message, "sender_id": self._sender_id}
        )
        if result.code != 0:
            raise SmsDeliveryError(f"telco error {result.code}: {result.detail}")

def send_login_code(sms: SmsSender, phone: str, code: str) -> None:
    sms.send(phone, f"Your login code is {code}")

if __name__ == "__main__":
    sms = TelcoSmsAdapter(TelcoClient(), "ACME")
    send_login_code(sms, "+4915112345678", "482913")
    try:
        send_login_code(sms, "+12", "000")
    except SmsDeliveryError as e:
        print("failed:", e)  # failed: telco error 21: invalid msisdn
```

```go title="notifications.go"
package main

import (
	"errors"
	"fmt"
	"strings"
)

// SmsSender is the target interface your code owns.
type SmsSender interface {
	Send(phone, message string) error
}

// --- Third-party SDK (simulated). You cannot edit this code. ---
type VendorPayload struct{ MSISDN, Text, SenderID string }

type VendorResult struct {
	Code   int
	Detail string
}

type TelcoClient struct{}

func (TelcoClient) Dispatch(p VendorPayload) VendorResult {
	n := len(p.MSISDN)
	if n < 8 || n > 15 || strings.Trim(p.MSISDN, "0123456789") != "" {
		return VendorResult{Code: 21, Detail: "invalid msisdn"}
	}
	fmt.Printf("[telco] %s -> %s: %s\n", p.SenderID, p.MSISDN, p.Text)
	return VendorResult{Code: 0, Detail: "queued"}
}

// --- end SDK ---

var ErrDelivery = errors.New("sms delivery failed")

type TelcoSmsAdapter struct {
	Client   TelcoClient
	SenderID string
}

func (a TelcoSmsAdapter) Send(phone, message string) error {
	r := a.Client.Dispatch(VendorPayload{
		MSISDN:   strings.TrimPrefix(phone, "+"),
		Text:     message,
		SenderID: a.SenderID,
	})
	if r.Code != 0 {
		return fmt.Errorf("%w: telco error %d: %s", ErrDelivery, r.Code, r.Detail)
	}
	return nil
}

func SendLoginCode(sms SmsSender, phone, code string) error {
	return sms.Send(phone, "Your login code is "+code)
}

func main() {
	var sms SmsSender = TelcoSmsAdapter{SenderID: "ACME"}
	fmt.Println(SendLoginCode(sms, "+4915112345678", "482913")) // <nil>
	err := SendLoginCode(sms, "+12", "000")
	fmt.Println(errors.Is(err, ErrDelivery), err)
}
```

```java title="Notifications.java"
public class Notifications {
    interface SmsSender {
        void send(String phone, String message);
    }

    // --- Third-party SDK (simulated). You cannot edit this code. ---
    record VendorPayload(String msisdn, String text, String senderId) {}

    record VendorResult(int code, String detail) {}

    static class TelcoClient {
        VendorResult dispatch(VendorPayload p) {
            if (!p.msisdn().matches("\\d{8,15}")) return new VendorResult(21, "invalid msisdn");
            System.out.printf("[telco] %s -> %s: %s%n", p.senderId(), p.msisdn(), p.text());
            return new VendorResult(0, "queued");
        }
    }
    // --- end SDK ---

    static class SmsDeliveryException extends RuntimeException {
        SmsDeliveryException(String message) { super(message); }
    }

    static final class TelcoSmsAdapter implements SmsSender {
        private final TelcoClient client;
        private final String senderId;

        TelcoSmsAdapter(TelcoClient client, String senderId) {
            this.client = client;
            this.senderId = senderId;
        }

        @Override
        public void send(String phone, String message) {
            VendorResult r = client.dispatch(
                new VendorPayload(phone.replaceFirst("^\\+", ""), message, senderId));
            if (r.code() != 0) {
                throw new SmsDeliveryException("telco error " + r.code() + ": " + r.detail());
            }
        }
    }

    static void sendLoginCode(SmsSender sms, String phone, String code) {
        sms.send(phone, "Your login code is " + code);
    }

    public static void main(String[] args) {
        SmsSender sms = new TelcoSmsAdapter(new TelcoClient(), "ACME");
        sendLoginCode(sms, "+4915112345678", "482913");
        try {
            sendLoginCode(sms, "+12", "000");
        } catch (SmsDeliveryException e) {
            System.out.println("failed: " + e.getMessage()); // failed: 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](https://aipatterns.dev/design-patterns/bridge.md) is designed up front so two hierarchies can vary independently. Adapter is applied after the fact to make existing parts fit.
- [Decorator](https://aipatterns.dev/design-patterns/decorator.md) keeps the interface and adds behavior. Adapter changes the interface and adds none.
- [Facade](https://aipatterns.dev/design-patterns/facade.md) defines a new, simpler interface over many classes. Adapter makes one class fit an interface that already exists.
- [Proxy](https://aipatterns.dev/design-patterns/proxy.md) 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.
