The Adapter Pattern: Making Incompatible Interfaces Work Together

2026-04-27

You've integrated a payment provider. Six months later, business wants to switch to a different one. If your code calls stripe.charges.create() directly in 47 places, you're facing a painful rewrite. The Adapter pattern prevents this by wrapping an incompatible interface behind one your code already expects.

The concept is simple: create a wrapper class that translates calls from your interface to the third-party one. Your application code never knows or cares what's behind the adapter.

Define the interface your application needs:

interface PaymentGateway {
  charge(amount: number, currency: string, token: string): Promise<PaymentResult>;
  refund(transactionId: string, amount: number): Promise<RefundResult>;
}

Then write thin adapters for each provider:

class StripeAdapter implements PaymentGateway {
  constructor(private client: Stripe) {}

  async charge(amount, currency, token) {
    const result = await this.client.charges.create({
      amount: amount * 100, // Stripe uses cents
      currency,
      source: token,
    });
    return { id: result.id, status: result.status };
  }
}

class BraintreeAdapter implements PaymentGateway {
  async charge(amount, currency, token) {
    const result = await this.gateway.transaction.sale({
      amount: amount.toString(), // Braintree wants strings
      paymentMethodNonce: token,
      options: { submitForSettlement: true },
    });
    return { id: result.transaction.id, status: result.success ? 'succeeded' : 'failed' };
  }
}

Notice how each adapter handles the quirks of its provider — Stripe wants cents, Braintree wants strings — while your application code just calls gateway.charge(29.99, 'usd', token) uniformly.

When to reach for the Adapter pattern:

Rule of thumb: If you import a third-party library in more than 3 files, you probably need an adapter. One import point means one place to change when the dependency evolves or gets replaced.

Common mistake: making the adapter too smart. An adapter should only translate between interfaces — no business logic, no caching, no retries. If you find your adapter growing beyond ~50 lines per method, you're likely mixing concerns. Put retry logic in a decorator, caching in a separate layer, and keep the adapter as a dumb translator.

This pattern pairs naturally with dependency injection (which you've already covered). Inject the adapter through your constructor, and swapping providers becomes a one-line configuration change rather than a codebase-wide search-and-replace.

See it in action: Check out 🔌 Adapter Design Pattern: Bridge Incompatible Interfaces Like a Pro Key Takeaway: Wrap third-party interfaces behind your own abstraction so that changing a dependency means writing one new adapter, not rewriting every call site.

All newsletters