Design Patterns, Explained Simply · Structural Patterns: Fitting Objects Together

Adapter

Make a class with the wrong interface work where your code expects another, by wrapping it in a small translator.

The problem

Your shop's checkout code works with a simple PaymentProvider interface: charge(cents, currency) returns a transaction id. Finance now requires an old in-house billing system for some customers. Its API is different: submit(amount, ccy) takes the amount as a string in dollars ("12.50") and returns a dictionary with a ref field.

You can't change the billing library (another team owns it), and you don't want if billing == "legacy" conversions sprinkled through checkout.

Real-world analogy

A travel plug adapter. Your laptop charger has a US plug; the hotel wall has a UK socket. You don't rewire the hotel or your charger. A small adapter in between translates one shape into the other.

Without the pattern

def checkout(provider, cents):
    if isinstance(provider, LegacyBilling):           # checkout now knows about every odd API
        result = provider.submit(f"{cents // 100}.{cents % 100:02d}", "USD")
        return result["ref"]
    return provider.charge(cents, "USD")

The pattern

  • Target: the interface your code expects (PaymentProvider.charge).
  • Adaptee: the existing class with an incompatible interface (LegacyBilling.submit).
  • Adapter: implements the target and translates each call to the adaptee: converting units, renaming methods, reshaping results.
  • Client: checkout, which only ever sees the target interface.

The code

Wrapping a legacy billing API behind our interfaceTry it yourself
class CardProvider:  # already matches our interface
    def charge(self, cents, currency):
        return f"card-{cents}-{currency}"


class LegacyBilling:  # third-party code we can't change
    def submit(self, amount, ccy):  # amount is a dollar string like "12.50"
        return {"ref": f"LB:{amount}:{ccy}", "status": "OK"}


class LegacyBillingAdapter:  # implements charge(), translates to submit()
    def __init__(self, legacy):
        self.legacy = legacy

    def charge(self, cents, currency):
        amount = f"{cents // 100}.{cents % 100:02d}"  # convert units
        result = self.legacy.submit(amount, currency)
        return result["ref"]  # reshape the result


def checkout(provider, cents):  # knows only charge()
    return f"paid, transaction {provider.charge(cents, 'USD')}"


print(checkout(CardProvider(), 1250))
print(checkout(LegacyBillingAdapter(LegacyBilling()), 1250))

Output

paid, transaction card-1250-USD
paid, transaction LB:12.50:USD

Checkout didn't change at all. Everything specific to the legacy API (dollar strings, the ref key) lives in one small class you can test on its own.

When to use it

  • You want to use an existing class (third-party SDK, legacy module, another team's service) whose interface doesn't match what your code expects.
  • You are wrapping vendors behind your own interface so you can switch providers later (payments, email, storage, maps).
  • You want to keep a third-party library's types from spreading through your codebase.

When not to use it

  • You own both sides and can simply change one interface to match. Do that instead.
  • The "adapter" would have to invent behaviour the adaptee doesn't support. That is a new implementation, not a translation.
  • Wrapping everything "just in case" adds a layer to read for no current benefit.

You've already used it

  • Java: Arrays.asList() adapts an array to the List interface; InputStreamReader adapts a byte stream to a character reader.
  • Laravel: the filesystem layer (Flysystem) adapts local disk, S3 and FTP to one Storage API. Its storage drivers are literally called adapters.
  • Python: DB-API drivers adapt each database's C library to the standard connect() / cursor() interface.

Key points

  • Adapter translates one interface into another, so incompatible classes can work together.
  • The client only knows the target interface; all translation lives in the adapter.
  • Great for wrapping third-party and legacy code and keeping vendor details out of your domain.
  • Further reading: Refactoring Guru: Adapter.

Practice questions

What problem does the Adapter pattern solve?

Answer

It lets a class with an incompatible interface work with code that expects a different interface, by wrapping it in an adapter that translates calls, units and results. The client and the adapted class stay unchanged.

Why is it useful to wrap third-party SDKs behind your own interface?

Answer

It keeps vendor-specific types and quirks out of your business logic, makes the code testable with fakes, and lets you switch or add providers by writing another adapter instead of editing every caller.

Give an example of Adapter in a well-known library.

Answer

Laravel's filesystem (built on Flysystem) adapts local disk, S3, FTP and others to one Storage API. In Java, Arrays.asList() adapts an array to the List interface and InputStreamReader adapts bytes to characters.

Open in the app to mark lessons complete, take notes and track your progress.