Adapter Design Pattern Examples for Real Integration Problems

Stackademic

See when the adapter pattern fits, how it differs from facade and decorator, and object adapter examples with testing tips.

The adapter design pattern lets incompatible interfaces work together without rewriting either side. You wrap an existing class (the adaptee) in a thin layer that exposes the interface your client code expects (the target).

Problem it solves

Imagine your checkout service expects a PaymentProcessor with charge(amount: Money): Receipt, but the only library available exposes payInCents(cardToken: string, cents: int): string. Changing the vendor library is expensive; changing every caller is risky.

An adapter implements PaymentProcessor internally by translating calls to payInCents. Callers stay clean; the vendor boundary stays isolated.

This shows up constantly at system boundaries: legacy APIs, third-party SDKs, different data shapes (JSON vs domain objects), and evolving microservice contracts.

Structure (GoF view)

RoleResponsibility
TargetInterface the client uses
AdapteeExisting component with incompatible API
AdapterImplements Target, delegates to Adaptee
ClientDepends on Target only

Two classic flavors:

  • Object adapter — composition: adapter holds a reference to the adaptee (preferred in most OOP languages).
  • Class adapter — inheritance: adapter subclasses both target and adaptee (possible in C++ multiple inheritance; awkward in Java/C# single inheritance).

Object adapter example (TypeScript)

interface Logger {
  log(message: string): void;
}

class WinstonLogger {
  info(msg: string, meta?: Record<string, unknown>): void {
    console.log("[winston]", msg, meta);
  }
}

class WinstonAdapter implements Logger {
  constructor(private readonly inner: WinstonLogger) {}

  log(message: string): void {
    this.inner.info(message, { source: "app" });
  }
}

function runApp(logger: Logger) {
  logger.log("Server started");
}

runApp(new WinstonAdapter(new WinstonLogger()));

runApp never imports Winston. Swapping logging backends means writing a new adapter, not rewriting business logic.

When to use an adapter

Good fits:

  • Integrating a third-party SDK behind your domain interface.
  • Gradual migration from OldService to NewService with a facade that speaks both during transition.
  • Test doubles where tests expect a narrow interface but production uses a bloated client.

Poor fits:

  • Greenfield code where you control both sides—align the interface instead of adapting forever.
  • Performance-critical inner loops where every call crosses translation layers—measure first.
  • Hiding badly designed APIs without documenting quirks—adapters can become dumping grounds.

Facade simplifies a subsystem (many classes) into one easier entry point. An adapter fixes one interface mismatch.

Decorator adds behavior while keeping the same interface. An adapter changes the interface to match what clients expect.

Anti-corruption layer (DDD term) is often implemented with adapters at bounded context edges—translate external models into your domain model aggressively, not field-by-field copy-paste.

Real-world scenarios

HTTP client versions — v1 API returns { "user_id": 1 }; v2 returns { "user": { "id": 1 } }. An adapter normalizes to UserId for the rest of the app while both APIs coexist.

Storage backends — Application speaks BlobStore.put(key, stream); AWS SDK uses multipart upload APIs. Adapter handles retries and part sizing.

UI components — Design system expects onChange(value: string); legacy date picker fires (year, month, day) integers. Adapter converts events.

Design guidelines

  1. Keep adapters thin. Business rules belong in domain services, not in translation glue.
  2. Document lossy conversions. If the adaptee cannot represent a concept, throw a typed error or return Option—do not silently drop data.
  3. One direction of dependency. Client → Target ← Adapter → Adaptee. Adaptee should not know about the adapter.
  4. Test mapping tables. Adapters are perfect for table-driven unit tests: input DTO → expected domain object.

Testing strategy

Unit-test the adapter with recorded fixtures from the real adaptee (sanitized JSON responses). Contract tests on the boundary catch vendor changes early.

For migrations, run parallel adapters behind a feature flag: old path vs new path, compare outputs in shadow mode before cutover.

Common mistakes

God adapter — 800 lines handling HTTP, caching, retries, and business rules. Split responsibilities.

Leaking adaptee types — returning vendor SDK objects through the Target interface. Wrap results in your types.

Permanent temporary adapter — “We’ll remove this after Q2” for three years. Track tech debt; schedule removal when traffic moves.

FAQ

Is every wrapper an adapter? Only if it specifically reconciles incompatible interfaces. Simple delegation without interface change is just composition.

Does REST use adapters? Conceptually yes—serializers map HTTP JSON to domain models. Teams rarely call them adapters, but the idea matches.

Functional languages? Adapters are often plain functions (ExternalRow) -> DomainModel without classes.

The adapter pattern earns its keep at boundaries you do not own. Used with discipline, it keeps core code stable while the messy outside world changes APIs, vendors, and data shapes underneath.

Java example (object adapter)

interface Analytics {
    void trackEvent(String name, Map<String, String> props);
}

final class VendorSdk {
    void send(String event, List<Pair> data) { /* vendor code */ }
}

final class VendorAnalyticsAdapter implements Analytics {
    private final VendorSdk sdk;

    VendorAnalyticsAdapter(VendorSdk sdk) {
        this.sdk = sdk;
    }

    @Override
    public void trackEvent(String name, Map<String, String> props) {
        List<Pair> pairs = props.entrySet().stream()
            .map(e -> new Pair(e.getKey(), e.getValue()))
            .toList();
        sdk.send(name, pairs);
    }
}

Domain code depends on Analytics; swapping vendors means a new adapter class, not a rewrite of checkout flows.

Python typing with Protocol

Python 3.10+ often uses typing.Protocol as the target interface:

from typing import Protocol

class Mailer(Protocol):
    def send(self, to: str, subject: str, body: str) -> None: ...

class SmtpLibAdapter:
    def __init__(self, client): 
        self._client = client

    def send(self, to: str, subject: str, body: str) -> None:
        self._client.mail(to, f"Subject: {subject}\n\n{body}")

Structural typing keeps adapters small without inheritance gymnastics.

Hexagonal architecture connection

Ports and adapters (hexagonal) generalize the idea: ports are interfaces your application defines; adapters are inbound (HTTP controllers) or outbound (database, messaging). The design pattern name “Adapter” often refers specifically to outbound translation at infrastructure edges.

When drawing architecture diagrams, label adapters explicitly so new hires do not bury vendor calls inside “service” classes.

Performance and caching

Adapters can cache read-heavy adaptee calls (memoize SDK metadata, TTL cache for feature flags). Keep cache invalidation documented—stale adapter caches cause subtle production bugs.

Avoid synchronous network I/O hidden inside adapters called from hot loops; batch or async where the adaptee allows.

Versioned adapters

When API v2 rolls out, run V1Adapter and V2Adapter implementing the same port, selected by configuration. Deprecate v1 with telemetry on which adapter path still receives traffic.

Code review questions for adapter PRs

  1. Does anything leak adaptee exceptions without mapping to domain errors?
  2. Are null/undefined cases from the adaptee handled explicitly?
  3. Is the adapter covered by tests using recorded fixtures?
  4. Could this logic move to a shared mapper used by multiple adapters?

FAQ (extended)

Adapter vs bridge pattern? Bridge separates abstraction from implementation hierarchies; adapter fixes one-off interface mismatch. They solve different problems.

Microservices — Anti-corruption layers between services often use adapter modules in the consumer to prevent foreign DTOs from spreading.

Generated clients — OpenAPI generators produce verbose clients; wrapping them behind a narrow port keeps application code readable.

When to delete an adapter — When the adaptee is gone and all callers use the native interface—or when the vendor SDK finally matches your port.