Beginner12 min readDesign Patternslive prototype

Adapter

Make two incompatible interfaces work together by putting a small translator between them. Your app expects `logger.log(message)`, but the third-party library only understands `writeXml(xmlString)` — like a round socket and a square plug. You can't change either side, so you wrap the library in an *adapter* that speaks both: it takes `log()` calls from your code and turns them into `writeXml()` calls for the library.

The idea

What it is

Adapter is the pattern you reach for when two pieces of code should work together but their interfaces don't line up. Think of a travel plug adapter: your laptop charger has one shape of plug, the wall in another country has a different shape of socket. You don't rewire the wall, and you don't cut the cable — you put a small converter between them that fits both. An Adapter class does exactly that for objects: it sits between your code and something with the wrong-shaped interface, translating calls from one form to the other.

Here's the running example for this whole lesson. Your app is written against a tiny Logger interface with one method: log(message). That's your round socket — dozens of places in your code call it. Now you bring in a great third-party library, XmlLoggerLib, but its only method is writeXml(xmlString) — a square plug. You can't edit the library (it's not your code), and you really don't want to hunt down every logger.log(...) call and rewrite it. So you write one small class, XmlLoggerAdapter, that implements Logger on the outside and calls XmlLoggerLib on the inside, wrapping each message as <log>message</log> along the way. Both sides stay untouched; only the adapter knows about the mismatch.

The one sentence to remember

An Adapter converts the interface of an existing class into the interface your code expects, so two classes that couldn't work together can — without changing either one.

You already use adapters every day

USB-C to HDMI dongles, card readers, power bricks — all real-world adapters. In code they're just as common: database drivers adapt vendor APIs to one standard interface, and wrapper classes adapt old legacy code to new interfaces. Whenever you see a class named SomethingAdapter or SomethingWrapper, this pattern is at work.

Mechanics

How it works

The three participants

Every Adapter setup has the same three roles. Name them and the pattern becomes easy to spot anywhere:

  • Target — the interface your client code expects. Here: Logger with log(message). This is the round socket.
  • Adaptee — the existing class with the useful behaviour but the wrong interface. Here: XmlLoggerLib with writeXml(xmlString). This is the square plug. You can't (or won't) change it.
  • Adapter — the small class in the middle. It implements the Target so the client can use it, and it holds the Adaptee inside so it can forward the real work — translating arguments, formats, or method names on the way through.
typescript
// TARGET — what the app expects (round socket)
interface Logger {
  log(message: string): void;
}

// ADAPTEE — third-party, can't change it (square plug)
class XmlLoggerLib {
  writeXml(xmlString: string): void { /* writes XML somewhere */ }
}

// ADAPTER — implements Target, wraps Adaptee
class XmlLoggerAdapter implements Logger {
  constructor(private lib: XmlLoggerLib) {}   // composition

  log(message: string): void {
    this.lib.writeXml(`<log>${message}</log>`);  // translate!
  }
}

The key line is inside log(): the adapter takes the call your app makes, reshapes the data into what the library wants, and forwards it. All the 'mismatch knowledge' lives in this one small class. And crucially, the client code never changes — every logger.log(...) in your app keeps working exactly as written. You just hand it an XmlLoggerAdapter instead of some other Logger.

Object adapter vs class adapter

There are two ways to build the adapter, and the difference is how it reaches the adaptee:

  • Object adapter (composition) — the adapter holds a reference to the adaptee and forwards calls to it. This is the version above, and the one you should use. It works in every language, it can adapt the adaptee and all its subclasses, and it keeps the two classes loosely coupled.
  • Class adapter (inheritance) — the adapter inherits from the adaptee (and the target at once). It needs multiple inheritance, so it only really works in languages like C++, and it welds the adapter to one concrete class. It saves a field, but that's about it.

Prefer composition

Recommend the object adapter by default: wrap, don't inherit. This is the same principle you met in [[composition-vs-inheritance]] — composition keeps the adapter flexible and testable, and it works everywhere. Reach for a class adapter only in the rare case where you must override some of the adaptee's own behaviour.

Don't confuse it with its neighbours

  • Adapter changes an interface's shape so existing code fits — it adds no new behaviour.
  • Facade ([[facade]]) doesn't translate one interface into another; it invents a new, simpler front door over a whole subsystem.
  • Decorator keeps the same interface and adds behaviour on top; Adapter changes the interface and adds nothing.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

A travel plug adapter for code. On the left, your app's card calls logger.log("User signed up") through a round socket. On the right, XmlLoggerLib.writeXml(xml) sticks out a square plug. Press ▶ Plug in directly and watch them clash — the shapes just don't fit. Press + Add adapter and a translator drops into the middle slot: round on the client side, square on the library side, with the mapping log(msg) → writeXml(<log>msg</log>) printed on it. Now press ▶ logger.log(...) and follow the message: it leaves the client as plain text, the adapter wraps it in XML, and the library flashes green as it receives exactly the format it wants. Keep an eye on the chip that reads client code changed: 0 lines — that's the whole point of the pattern.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Feel the mismatch

Open the prototype and press '▶ Plug in directly'. The square plug lunges at the round socket, both cards shake, and a red ✗ lands in the slot: 'Interfaces don't match.' This is your app trying to call writeXml-shaped code through a log-shaped interface — it simply can't connect. Note the chip at the top: 'client code changed: 0 lines'. We're going to fix this without ever touching the client.

try 02

Drop in the adapter

Press '+ Add adapter'. A translator piece fills the middle slot — rounded on the client side, square on the library side — printed with the mapping log(msg) → writeXml(<log>msg</log>). That one line is the entire pattern: implement the interface the client expects, forward to the method the library provides. Notice the '▶ logger.log(...)' button lights up: the circuit is now complete.

try 03

Watch the translation

Press '▶ logger.log(...)' and follow the message across the stage: it leaves the client as plain text "User signed up", the adapter wraps it into <log>User signed up</log>, and the library card flashes green as it receives valid XML. Now check the chip again — still 'client code changed: 0 lines'. The client kept calling log(), the library kept receiving writeXml(), and only the adapter knew about the mismatch. Press '↺ Reset' and run it once more until the flow feels obvious.

In practice

When to use it — and what trips people up

Reach for an Adapter when you can't change one of the sides

Adapter earns its keep whenever useful code exists but its interface doesn't match what your code expects — and rewriting either side is off the table:

  • Integrating third-party or legacy code — a library, SDK, or ancient in-house module does exactly what you need, but its method names and data formats don't match yours. Wrap it once; use it everywhere.
  • Making old code fit a new interface — you've designed a clean new interface and want existing classes to satisfy it without risky rewrites. One adapter per old class bridges the gap.
  • Adapters at system boundaries — payment providers, logging backends, storage services. Define one Target interface for your app, then write a thin adapter per vendor. Swapping vendors becomes swapping adapters, and your core code never hears about it.

When NOT to use it

If you own both sides, don't write an adapter — just change the code so the interfaces match. An adapter between two classes you control is pure overhead: an extra class, an extra hop, and a permanent reminder of a mismatch you could have simply fixed. And if you find yourself stacking adapters on adapters, stop and redesign the interface instead.

What it gives you

  • Reuses existing code you can't change — third-party libraries and legacy classes plug straight into your design without a rewrite.
  • Client code stays untouched — every existing call site keeps working; you only add one small class and swap what gets passed in.
  • Single responsibility for the mismatch — all the translation logic (formats, names, units) lives in one findable place instead of being smeared across the codebase.
  • Makes swapping implementations easy — one adapter per vendor behind a shared Target interface turns 'migrate providers' into 'write one new adapter'.

Common mistakes

  • Adds a layer of indirection — one more class, one more hop per call, one more thing to name and navigate when reading the code.
  • Can hide a design smell — sprinkling adapters between classes you own papers over interfaces that should just be fixed at the source.
  • Translation can be lossy or leaky — if the two interfaces don't map cleanly (missing features, different error models), the adapter ends up faking or dropping behaviour.
  • Adapter chains creep in — adapting an adapter of an adapter makes call flows genuinely hard to trace; a redesign is usually cheaper than a third layer.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// TARGET — the interface your app already expects (round socket)
interface Logger {
  log(message: string): void;
}

// ADAPTEE — third-party library, you can't change it (square plug)
class XmlLoggerLib {
  writeXml(xmlString: string): void {
    console.log("[xml-logger]", xmlString);
  }
}

// ADAPTER — implements the Target, wraps the Adaptee (composition)
class XmlLoggerAdapter implements Logger {
  constructor(private lib: XmlLoggerLib) {}

  log(message: string): void {
    // the translation: plain text -> the XML the library wants
    this.lib.writeXml(`<log>${message}</log>`);
  }
}

// CLIENT — written against Logger; it never changes
function signUpUser(logger: Logger): void {
  logger.log("User signed up");
}

// plug the adapter in where a Logger is expected
signUpUser(new XmlLoggerAdapter(new XmlLoggerLib()));
// [xml-logger] <log>User signed up</log>

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What problem does the Adapter pattern solve?

question 02 / 05

In our example, the app expects Logger.log(message) but the library only has XmlLoggerLib.writeXml(xmlString). Which role does XmlLoggerLib play?

question 03 / 05

How does a well-built object adapter connect the Target to the Adaptee?

question 04 / 05

After you introduce XmlLoggerAdapter, what happens to the client code that calls logger.log(...)?

question 05 / 05

When should you NOT write an adapter?

0/5 answered