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:
Loggerwithlog(message). This is the round socket. - Adaptee — the existing class with the useful behaviour but the wrong interface. Here:
XmlLoggerLibwithwriteXml(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.
// 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.
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.
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.
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- Articlerefactoring.guru
Adapter — Refactoring.Guru
The clearest illustrated walkthrough: the round-peg/square-peg problem, object vs class adapter structure, and examples in many languages. Best first read.
- Booken.wikipedia.org
Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides
The original 'Gang of Four' book that catalogued Adapter among the structural patterns. The primary source for the pattern's intent, participants, and consequences.
- Docsen.wikipedia.org
Adapter pattern — Wikipedia
Concise reference with UML for both object and class adapters, plus real API examples and the distinction from Facade and Decorator.
- Articlesourcemaking.com
Adapter Design Pattern — SourceMaking
Short problem/solution framing with checklists and rules of thumb — good for a quick second angle on when the pattern applies.
- Articlebaeldung.com
The Adapter Pattern in Java — Baeldung
A compact Java implementation walkthrough with a practical example, plus notes on where the JDK itself uses adapters.
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