designpattern.site

Adapter Pattern

Convert the interface of a class into another interface that clients expect, letting incompatible interfaces work together.

Overview

Software rarely lives in isolation. Libraries get replaced, third-party APIs evolve, and legacy subsystems must coexist with new code. When two components need to collaborate but their interfaces do not match, you have a few options: rewrite one of them, add conditionals everywhere, or introduce an Adapter.

The Adapter pattern is a structural design pattern that acts as a translator between two incompatible interfaces. It wraps an existing class (the adaptee) and exposes the interface the calling code (the client) actually expects, without changing either the client or the adaptee.

Think of a travel power adapter. Your laptop charger has a two-pin plug; the wall socket has three holes. The adapter sits between them, converting one physical interface to the other. Neither the charger nor the wall socket changes — only the adapter bridges the gap.

Problem & Motivation

Suppose your application uses a PaymentProcessor interface to charge customers. You have one implementation for Stripe and the app works well. Then the business team contracts with a new provider, LegacyPayGateway, whose API was written in 2008 and looks nothing like PaymentProcessor. It uses different method names, accepts differently shaped arguments, and returns XML instead of a structured object.

Your options without Adapter:

  • Rewrite LegacyPayGateway — you do not own or control the third-party library; this is not possible.
  • Modify the client code — sprinkle if (provider === "legacy") branches throughout the checkout flow, coupling business logic to infrastructure details.
  • Write a wrapper class — this is exactly the Adapter pattern, but done ad hoc and without a consistent structure.

The Adapter pattern formalizes the wrapper approach. You create an AdapterClass that implements PaymentProcessor and internally delegates to LegacyPayGateway, translating between the two interfaces in one place. All client code continues to use PaymentProcessor unchanged.

There are two classical variants:

  • Object Adapter — holds a reference to an instance of the adaptee. This is the more common and flexible approach because it works even when the adaptee is a final class or a third-party type you cannot subclass.
  • Class Adapter — inherits from both the target interface and the adaptee (multiple inheritance). Available in languages that support it, such as Python and C++. Java and Go do not support multiple inheritance of classes.

Class Diagram

Loading diagram...

The key structural points are:

  • PaymentProcessor is the target interface — what the client expects.
  • LegacyPayGateway is the adaptee — the incompatible class that already exists.
  • LegacyPayAdapter is the adapter — it implements the target interface and holds a reference to the adaptee.
  • The client only ever depends on PaymentProcessor. It has no knowledge of LegacyPayGateway.

Implementation

The examples below adapt LegacyPayGateway (which works in integer cents and returns raw strings) to the PaymentProcessor interface (which works in decimal amounts and returns structured receipts). Both object adapter and class adapter approaches are shown where the language supports it.

// Target interface — what the client expects
interface Receipt {
  transactionId: string;
  amount: number;
  currency: string;
  success: boolean;
}

interface PaymentProcessor {
  charge(amount: number, currency: string): Receipt;
  refund(transactionId: string): boolean;
}

// Adaptee — the incompatible third-party class
class LegacyPayGateway {
  makePayment(amountCents: number, currencyCode: string): string {
    const txId = `TXN-${Date.now()}`;
    console.log(`[Legacy] Charging ${amountCents} cents (${currencyCode})`);
    return txId; // returns a raw transaction ID string
  }

  reverseTransaction(txId: string): number {
    console.log(`[Legacy] Reversing transaction ${txId}`);
    return 0; // returns 0 for success, non-zero for failure
  }
}

// Object Adapter — wraps an instance of LegacyPayGateway
class LegacyPayAdapter implements PaymentProcessor {
  private gateway: LegacyPayGateway;

  constructor(gateway: LegacyPayGateway) {
    this.gateway = gateway;
  }

  charge(amount: number, currency: string): Receipt {
    const amountCents = Math.round(amount * 100);
    const txId = this.gateway.makePayment(amountCents, currency.toUpperCase());
    return { transactionId: txId, amount, currency, success: true };
  }

  refund(transactionId: string): boolean {
    const statusCode = this.gateway.reverseTransaction(transactionId);
    return statusCode === 0;
  }
}

// Concrete implementation using the target interface directly
class StripeProcessor implements PaymentProcessor {
  charge(amount: number, currency: string): Receipt {
    const txId = `STR-${Date.now()}`;
    console.log(`[Stripe] Charging ${amount} ${currency}`);
    return { transactionId: txId, amount, currency, success: true };
  }

  refund(transactionId: string): boolean {
    console.log(`[Stripe] Refunding ${transactionId}`);
    return true;
  }
}

// Client code — works with any PaymentProcessor
function processOrder(processor: PaymentProcessor, total: number): void {
  const receipt = processor.charge(total, "USD");
  console.log(`Order charged. Transaction ID: ${receipt.transactionId}`);
}

// Usage
const stripe = new StripeProcessor();
const legacy = new LegacyPayAdapter(new LegacyPayGateway());

processOrder(stripe, 49.99);  // [Stripe] Charging 49.99 USD
processOrder(legacy, 49.99);  // [Legacy] Charging 4999 cents (USD)

Object Adapter vs Class Adapter

The object adapter (composition) is preferred in almost every case. It works regardless of whether the adaptee is a final class, an external library type, or a struct you cannot inherit from. The class adapter (inheritance) is only viable in languages that support multiple inheritance — Python and C++ being the common examples. Even then, prefer composition unless you have a specific reason to inherit, because deep inheritance hierarchies are harder to follow and test.

Real-World Examples

Third-party payment gateways — Stripe, PayPal, and Adyen each expose different API shapes. A payment abstraction layer in an e-commerce platform wraps each provider in its own adapter that implements a common PaymentGateway interface. Switching providers becomes a one-line configuration change rather than a codebase-wide refactor.

XML-to-JSON data sources — A reporting dashboard expects data in JSON format. An older internal service returns XML. Rather than modifying either the dashboard or the legacy service, an XmlDataAdapter fetches the XML, parses it, and returns it as a JSON-compatible structure. The dashboard never knows XML existed.

Logging libraries — Applications often abstract logging behind a Logger interface so the underlying library (Winston, Pino, log4j, slog) can be swapped. Each library has its own method names and configuration — adapters bridge from the generic Logger interface to the specific library's API. This is exactly how SLF4J works in the Java ecosystem: it is an adapter layer over Logback, Log4j2, and others.

File system and cloud storage — A file storage abstraction accepts read(path) and write(path, data) calls. Adapters translate these calls to the S3 SDK, the GCS client, or a local filesystem. Application code calls the same interface whether running locally or in production.

Pros and Cons

Advantages

Single Responsibility Principle — interface translation logic lives entirely inside the adapter. The client and the adaptee stay focused on their own concerns.

Open/Closed Principle — you add adapters for new providers without modifying existing client code or existing adapters.

Reuse incompatible classes — third-party and legacy code that you cannot modify becomes usable without a rewrite.

Testability — because the client depends on an interface, you can substitute a mock or a stub in tests, regardless of which real adapter is in production.

Disadvantages

Indirection overhead — every call passes through an extra layer. For most business logic this is negligible, but in hot loops or high-throughput pipelines the extra method dispatch adds up.

Proliferation of adapter classes — a system integrating many third-party services can accumulate dozens of adapters. Without careful organization this adds to cognitive load.

Leaky abstractions — if the adaptee's behavior fundamentally differs from the target interface (e.g., one is synchronous, the other async), the adapter must handle the mismatch, which can make it complex or imperfect.

Hides complexity — a simple adapter wrapper can obscure the fact that an underlying service is slow, unreliable, or has rate limits. Observability (logging, metrics) inside the adapter is important.

When to Use / When to Avoid

Use Adapter when:

  • You want to use an existing class but its interface is incompatible with the rest of your code.
  • You are integrating a third-party library or a legacy subsystem you cannot modify.
  • You want to create a reusable class that works with interfaces not yet known at design time.
  • You need to support multiple external providers behind a single stable interface.

Avoid Adapter when:

  • The interfaces are similar enough that a direct call (with minor argument massaging) is cleaner than a full adapter class.
  • You control both sides of the interface — if you can change the adaptee, change it rather than wrapping it.
  • The semantic mismatch is so large that the adapter becomes a non-trivial translation layer with its own bugs and edge cases; at that point, a dedicated anti-corruption layer or a more explicit mapping step is cleaner.
  • Performance is critical and the added indirection is measurable — prefer direct access in that case.

Facade — both patterns wrap existing code, but for different reasons. Adapter makes an incompatible interface compatible. Facade simplifies a complex subsystem behind an easier interface without necessarily changing the interface shape. You might use both: a Facade over a complex subsystem, and an Adapter to make that Facade fit a standard protocol.

Decorator — also wraps an object, but preserves the same interface while adding behavior. Adapter changes the interface; Decorator extends the interface's behavior without altering its shape.

Bridge — separates an abstraction from its implementation so both can vary independently. Bridge is designed upfront as part of the architecture; Adapter is applied retroactively to make existing, incompatible code work together.

Proxy — wraps an object and keeps the same interface, adding cross-cutting concerns (caching, access control, lazy loading). Adapter changes the interface; Proxy preserves it.

Design Tip

If you find yourself writing adapter after adapter for the same category of incompatibility — say, every data source returns a different format — consider defining a formal port-and-adapter architecture (Hexagonal Architecture). The pattern stays the same, but naming your adapters as "ports" and organizing them in a dedicated layer makes the intent explicit and keeps your domain model free from infrastructure details.

On this page