Adapter Pattern

intermediate 2-4 YOE lld design-pattern structural

The Adapter pattern lets us make two incompatible interfaces work together. We wrap one class with a new class that translates its interface into something the rest of our code expects.

Think of it like a power adapter. We have a US plug, but we’re in Europe. The plug works fine — we just need an adapter in between to make the shapes match. The adapter doesn’t change the electricity or the device. It just makes them compatible.

The Problem It Solves

We’re building a payment system. Our app expects every payment gateway to have a processPayment(amount) method. Then our company integrates a third-party library whose class has a completely different method: makeTransaction(dollarAmount, currency).

We can’t change the third-party code. We can’t rewrite our whole app. We need an adapter that makes the third-party class look like our expected interface.

How It Works

Adapter Pattern
Client
expects processPayment()
Adapter
processPayment() →
translates to →
makeTransaction()
Legacy Gateway
has makeTransaction()
The client never knows it's talking to a legacy system. The adapter handles the translation.

The adapter:

  1. Implements the target interface (what the client expects)
  2. Holds a reference to the adaptee (the incompatible class)
  3. Translates calls from the target interface to the adaptee’s methods

Code Implementation

from abc import ABC, abstractmethod

# Target interface -- what our app expects
class PaymentGateway(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> str:
        pass

# Our existing gateway -- works perfectly
class StripeGateway(PaymentGateway):
    def process_payment(self, amount: float) -> str:
        return f"Stripe: charged ${amount}"

# Third-party legacy gateway -- DIFFERENT interface
class LegacyPayPal:
    def make_transaction(self, dollar_amount: float, currency: str) -> str:
        return f"PayPal: sent {currency} {dollar_amount}"

# Adapter -- wraps LegacyPayPal to look like PaymentGateway
class PayPalAdapter(PaymentGateway):
    def __init__(self, paypal: LegacyPayPal):
        self._paypal = paypal

    def process_payment(self, amount: float) -> str:
        # Translate our interface to the legacy one
        return self._paypal.make_transaction(amount, "USD")

# Client code -- doesn't care about the underlying implementation
def checkout(gateway: PaymentGateway, amount: float):
    result = gateway.process_payment(amount)
    print(result)

checkout(StripeGateway(), 49.99)               # Stripe: charged $49.99
checkout(PayPalAdapter(LegacyPayPal()), 49.99)  # PayPal: sent USD 49.99
// Target interface (via duck typing in JS)
class StripeGateway {
  processPayment(amount) {
    return `Stripe: charged $${amount}`;
  }
}

// Third-party legacy gateway -- DIFFERENT interface
class LegacyPayPal {
  makeTransaction(dollarAmount, currency) {
    return `PayPal: sent ${currency} ${dollarAmount}`;
  }
}

// Adapter -- wraps LegacyPayPal to match our expected interface
class PayPalAdapter {
  #paypal;

  constructor(paypal) {
    this.#paypal = paypal;
  }

  processPayment(amount) {
    // Translate our interface to the legacy one
    return this.#paypal.makeTransaction(amount, "USD");
  }
}

// Client code
function checkout(gateway, amount) {
  console.log(gateway.processPayment(amount));
}

checkout(new StripeGateway(), 49.99);                   // Stripe: charged $49.99
checkout(new PayPalAdapter(new LegacyPayPal()), 49.99); // PayPal: sent USD 49.99
// Target interface
interface PaymentGateway {
    String processPayment(double amount);
}

class StripeGateway implements PaymentGateway {
    public String processPayment(double amount) {
        return "Stripe: charged $" + amount;
    }
}

// Third-party legacy gateway -- different interface
class LegacyPayPal {
    public String makeTransaction(double dollarAmount, String currency) {
        return "PayPal: sent " + currency + " " + dollarAmount;
    }
}

// Adapter
class PayPalAdapter implements PaymentGateway {
    private final LegacyPayPal paypal;

    PayPalAdapter(LegacyPayPal paypal) {
        this.paypal = paypal;
    }

    public String processPayment(double amount) {
        return paypal.makeTransaction(amount, "USD");
    }
}

// Usage:
// PaymentGateway gw = new PayPalAdapter(new LegacyPayPal());
// System.out.println(gw.processPayment(49.99));

Class Adapter vs Object Adapter

There are two ways to build an adapter:

Object AdapterClass Adapter
HowWraps the adaptee (composition)Extends the adaptee (inheritance)
FlexibilityCan adapt any subclass of adapteeOnly adapts the specific class
RecommendedYes — composition over inheritanceRarely, requires multiple inheritance

We almost always use Object Adapter (composition). It’s more flexible and follows the “favor composition over inheritance” principle.

When to Use

  • Integrating a third-party library with an incompatible interface
  • Working with legacy code that can’t be modified
  • Making unrelated classes work together
  • Wrapping a library so we can swap implementations later

When NOT to Use

  • When we can just modify the source code — don’t add an adapter if we own both sides
  • When the interfaces are too different — if the translation is extremely complex, an adapter might not be the right fit

In simple language, an Adapter is a translator. It sits between two classes that speak different languages and makes them understand each other. We don’t change either class — we just add a middleman that handles the translation.