What Is the Adapter Pattern?
A travel plug adapter does not rebuild your charger or rewire the foreign wall socket — it simply sits between the two, translating one shape into the other. The Adapter pattern gives your code that exact same quiet, translating superpower.
The Big Idea, in One Breath
Wrap an existing class with a mismatched interface, translating its methods into the shape your code actually expects — without changing a single line of the original class.
Imagine landing in a country where the wall sockets look nothing like the plug on your phone charger. You do not take a screwdriver to the wall, and you certainly do not rebuild your charger from scratch. You simply reach for a small travel adapter — a little block that plugs into their socket on one side and accepts your charger’s plug on the other. Nothing about your charger changes. Nothing about their electrical system changes. The adapter just quietly sits in the middle, translating one shape into the other, and suddenly the two, which were never designed with each other in mind, work together perfectly.
The Adapter pattern brings that exact same quiet trick into software. It is a design pattern that wraps an existing class with a mismatched interface, translating its methods into the shape your code actually expects — without changing a single line of the original class, and without forcing your own code to bend around the mismatch either.
Think about a universal remote control. It does not rebuild your television, your speaker system, or your streaming box — it simply sits in front of all three, offering one familiar set of buttons that quietly translates your presses into whatever specific signal each individual device actually expects. You press “volume up” once, and the remote figures out how to say that in whichever device’s own private language. The Adapter pattern plays this exact translating role for pieces of code that were never designed to speak the same language.
What makes this small trick so quietly powerful is how little it asks of either side. The wall socket in a foreign country never has to change to accommodate travellers from every corner of the world, and your phone charger never has to be redesigned every time you cross a border. Each side keeps doing exactly what it already does best, fully unaware that the other even exists in a different shape. The adapter absorbs all of that awkwardness itself, standing quietly in the middle so that neither side ever has to compromise on its own design just to cooperate with the other.
What the Adapter Pattern Really Is
A structural design pattern that converts the interface of one class into another interface a client expects, allowing incompatible classes to work together without editing either.
Formally, the Adapter pattern is a structural design pattern that converts the interface of one class into another interface that a client expects, allowing classes to work together that otherwise could not because of incompatible interfaces. It belongs to the structural family of patterns — the group concerned with how existing classes and objects are arranged and connected — rather than the creational family concerned with how objects are built, or the behavioural family concerned with how objects communicate over time.
- Converting an interface. The adapter does not change what an existing class does internally; it only changes the shape of how you talk to it.
- Making the incompatible compatible. Two pieces of code that were never designed with each other in mind can now cooperate, without either one being rewritten.
“What does the Adapter pattern actually do?” is really asking: it lets your code keep speaking the language it already knows, while quietly translating that language into whatever a mismatched, older, or third-party piece of code happens to speak instead.
It is worth being precise about what makes this genuinely different from simply editing the original class to match what you need. Adapter specifically assumes you either cannot or should not touch that original class — perhaps it belongs to a third-party library, perhaps it is shared by other parts of a large system, or perhaps it is simply old, trusted code nobody wants to risk destabilising. The adapter sits entirely outside it, leaving the original untouched.
There is a subtler idea worth naming explicitly here too: the pattern is fundamentally about respecting boundaries that already exist, rather than trying to erase them. A large software system is often made up of pieces built by different teams, at different times, sometimes even by entirely different companies, each with their own reasonable design choices baked in. Adapter accepts that reality rather than fighting it — instead of insisting everyone should have designed things the same way from the start, it simply builds a small, honest bridge between whatever already exists and whatever your own code currently needs, letting both sides keep their own integrity intact.
A Little History
One of the seven structural patterns catalogued in 1994 by the Gang of Four. Sits alongside Bridge, Composite, Decorator, Facade, Flyweight, and Proxy.
Adapter is one of the seven structural patterns catalogued in 1994 by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — the four authors nicknamed the “Gang of Four” — in their book Design Patterns: Elements of Reusable Object-Oriented Software. It sits alongside Bridge, Composite, Decorator, Facade, Flyweight, and Proxy as one of the foundational ways of arranging existing classes and objects into larger, more capable structures.
The underlying idea long predates the formal 1994 catalogue, and in fact predates computing altogether — the physical adapter plug this pattern is so often compared to had already been solving exactly this kind of “these two things were not built for each other, but need to work together anyway” problem for decades. Software engineers, noticing the same shape of problem appearing constantly in their own work — an old billing system that needed to talk to a new reporting tool, a well-tested library whose interface did not quite match a new project’s needs — simply borrowed a name that was already perfectly intuitive.
Some engineers refer to this same pattern by an alternate name, “Wrapper,” since an adapter quite literally wraps an existing class in a new, compatible outer layer. Both names describe the exact same underlying idea, and you will see both used interchangeably across different codebases and teams.
It is worth appreciating why Adapter has aged so gracefully compared to some of its 1994 contemporaries. The problem it solves — two things that were not designed together, needing to cooperate anyway — has not become any less common with time; if anything, it has grown considerably more common. Modern software rarely exists in isolation anymore. A typical application today might depend on a dozen third-party services, several older internal systems still running from years past, and a handful of open-source libraries, each with its own design conventions. Every one of those integration points is a potential opportunity for exactly the kind of mismatch Adapter was built to solve, which is precisely why the pattern has remained just as relevant, if not more so, three decades after it was first formally catalogued.
Why Not Just Rewrite the Old Class?
Four very common, very real reasons rewriting the original is not on the table — and one broader reason it is often unwise even when technically possible.
It is a fair question — if a class’s interface does not match what you need, why not simply edit it directly? In practice, there are several very common, very real reasons that option is not available.
You do not own it
A library or SDK provided by another company cannot be edited directly — you can only use it exactly as it was published.
Others depend on it too
An older class used by several other parts of a large system cannot be safely rewritten without risking every other piece that already depends on it.
You cannot even see it
Some external services and compiled libraries do not expose their internal source code at all, making direct edits physically impossible.
Many callers, many expectations
Different parts of a system may need the exact same underlying class to look completely different, which a single edited version could never satisfy for everyone at once.
Even when editing the original class is technically possible, it is often simply unwise. A class that has been running reliably in production for years carries real, earned trust — every edit to it, however small, reopens the risk of breaking something quietly relying on its current, exact behaviour. An adapter sidesteps all of this risk entirely, by leaving the original class completely untouched and doing all the necessary translation work in a new, separate, easily tested piece of code sitting just outside it.
It is worth walking through what “risk” actually means in this context, since it is easy to underestimate until it is experienced firsthand. Imagine a payroll system’s core calculation class, quietly relied upon by dozens of other pieces of code across an organisation — some of them written years ago, by engineers who have long since moved on, with no complete record of every single place that depends on its exact current behaviour. Editing that class directly, even to fix what looks like a small, obviously beneficial improvement, risks breaking something distant and unrelated in a way that might not surface for weeks. An adapter placed alongside it, translating its interface for one new, specific caller, carries none of that same risk — if the adapter itself has a bug, it affects only the new code using it, leaving the trusted, battle-tested original completely undisturbed.
The Anatomy of the Adapter Pattern
Four cooperating roles: Target, Client, Adaptee, Adapter. The Client only ever talks to Target; the Adapter secretly translates those calls into whatever the Adaptee actually understands.
A properly structured Adapter implementation is built from four cooperating roles.
- Target. The interface your code already expects and knows how to use.
- Client. The code that wants to work with something through the Target interface, unaware of any mismatch happening behind the scenes.
- Adaptee. The existing class with the incompatible interface — often old, third-party, or otherwise untouchable.
- Adapter. The new class that implements Target, while internally holding and translating calls to the Adaptee.
It is worth paying close attention to the Client’s role in this diagram, because its complete unawareness is really the entire point. The Client was written, and could have been written, entirely without knowing that an Adaptee, or an Adapter, would ever exist. It simply expects something shaped like Target, and as long as whatever it is handed satisfies that shape, everything works — whether that is a “real,” originally intended implementation of Target, or an Adapter secretly translating calls to something completely different underneath. This is a direct, practical expression of a broader idea in good software design sometimes called “programming to an interface, not an implementation” — the Client only cares about the shape of what it is given, never the specific class actually providing it.
Building an Adapter, Step by Step
A small legacy printer that speaks the wrong language. Our own code, unchanged. A tiny adapter class bridging the two — nothing else touched.
Seeing the pattern actually assembled, from a genuine mismatch to a working solution, makes its structure far easier to hold onto than reading about it in the abstract.
class LegacyPrinter: # An old, trusted class we are not allowed to touch def old_print_text(self, text): return f"[legacy] {text}" def send_to_printer(printer): # Our own code, already written, expects a .print() method return printer.print("Invoice ready")
Calling send_to_printer(LegacyPrinter()) right now would crash — LegacyPrinter has no print() method at all, only old_print_text(). Rewriting LegacyPrinter is not an option, since other parts of the system already depend on its exact current interface. An adapter solves this cleanly.
class PrinterAdapter: def __init__(self, legacy_printer): self._legacy = legacy_printer def print(self, text): # Translate the call our code expects into the call # the legacy class actually understands. return self._legacy.old_print_text(text) adapter = PrinterAdapter(LegacyPrinter()) print(send_to_printer(adapter)) # works — no changes to either side
Notice what did not change: LegacyPrinter stayed exactly as it was, and send_to_printer() stayed exactly as it was too. The only new thing added is PrinterAdapter, a small, self-contained bridge sitting quietly between the two, translating one call into the other.
It is worth pausing on just how small and focused that new piece of code turned out to be. PrinterAdapter does exactly one job — translate print() into old_print_text() — and nothing more. It does not add new features to the printer, it does not change what “printing” means, and it does not quietly reach in and start relying on other internal details of LegacyPrinter beyond the one method it needs. That narrow, disciplined focus is precisely what makes adapters so easy to test, review, and trust: a reviewer looking at this class does not need to understand the entire legacy printing system to confirm it is correct, only that one line, translating one call into another, faithfully does what it claims.
Object Adapter vs. Class Adapter
Two distinct GoF variations. Object adapter uses composition and is by far the more common modern choice. Class adapter uses multiple inheritance and only works in languages that support it.
The Gang of Four originally described two distinct ways of building an adapter, and it is worth understanding both, even though one has become considerably more common in modern practice.
| Approach | How It Works | Trade-off |
|---|---|---|
| Object Adapter | Holds a reference to the Adaptee, using composition | Works in any language; can adapt subclasses of the Adaptee too |
| Class Adapter | Inherits from the Adaptee directly, using multiple inheritance | Only possible in languages supporting multiple inheritance |
The earlier PrinterAdapter example used the object adapter approach — holding a reference to LegacyPrinter inside itself. This has become the dominant, most widely recommended style, largely because many popular modern languages, including Java and C#, do not support multiple inheritance at all, ruling out the class adapter approach entirely. Even in languages like Python or C++ that do allow it, most engineers still favour the object adapter’s composition-based approach for the same reason composition is generally favoured throughout modern object-oriented design: it stays more flexible, and avoids permanently locking the adapter into one specific class hierarchy.
There is a practical benefit to the object adapter approach worth spelling out concretely: because it simply holds a reference to the Adaptee rather than inheriting from it, the exact same adapter class can, in principle, work with any object that behaves the way the Adaptee is expected to behave — including future subclasses of the Adaptee that do not even exist yet at the time the adapter is written. A class adapter, by contrast, is permanently welded to one specific Adaptee class through inheritance, unable to flexibly adapt anything else without an entirely separate, near-duplicate adapter class being written from scratch.
Real-World Use Cases
Wherever old and new, or internal and external, need to cooperate — legacy integrations, third-party APIs, data-format bridges, UI library migrations.
Adapter shows up constantly in real, professional software, especially anywhere old and new, or internal and external, pieces of code need to cooperate.
Old systems, new features
A company modernising part of its codebase can wrap an old, trusted billing system in an adapter, letting new code use it through a clean, modern interface.
Swappable external services
Wrapping a specific payment provider’s SDK behind an adapter matching your own app’s interface makes switching providers later far less disruptive.
Translating between formats
An adapter can accept data in one format — say, XML — and expose it through an interface the rest of an application already expects, like JSON-style objects.
Mismatched component interfaces
Adopting a new UI library with a different API from the one currently in use can be eased by an adapter layer, avoiding an all-at-once, risky rewrite.
Adapter is especially prized in large organisations undergoing gradual modernisation, since it allows new, clean code to be written against a stable, well-designed interface today, while the messy legacy system underneath continues running, untouched, exactly as it always has — until the day, if it ever comes, that it is finally replaced for good.
Cloud migration projects offer a particularly clear, concrete illustration of this benefit. A company moving from an old, self-hosted storage system to a modern cloud storage provider rarely flips every single piece of code over in one risky, all-at-once weekend. Instead, an adapter matching the old storage system’s interface is written around the new cloud provider’s SDK, letting every existing piece of code that already expects the old interface keep working completely unmodified, while new code can gradually be written to talk to the cloud provider more directly over time. This staged, adapter-driven approach turns what could be a single high-stakes, all-or-nothing migration into a much calmer, incremental transition — exactly the kind of practical, risk-reducing value that makes this pattern such an enduring favourite among architects managing large, evolving systems.
Adapter vs. Facade vs. Decorator
All three structural patterns involve wrapping something. The distinction lies entirely in why the wrapping happens.
These three structural patterns are frequently confused, since all three involve wrapping something. The distinction lies entirely in why the wrapping happens.
| Pattern | Purpose of Wrapping | Changes the Interface? |
|---|---|---|
| Adapter | Translate an incompatible interface into one that is expected | Yes — that is the entire point |
| Facade | Simplify a complicated subsystem behind one clean entry point | Simplifies it, rather than translating it |
| Decorator | Add new behaviour to an object, layer by layer | No — it keeps the same interface, adding to it |
A useful way to hold all three apart: Adapter answers “how do I make this fit?” Facade answers “how do I make this simpler?” Decorator answers “how do I add more to this, without disturbing what is already there?” An adapter wraps something that is incompatible; a facade wraps something that is merely complicated; a decorator wraps something that is already working fine, just to enhance it further.
It helps to see all three side by side on the exact same starting material. Imagine a messy, old email-sending library with a dozen confusing configuration options. If your own code expects a clean, simple send(to, message) method that this library does not naturally offer, that is Adapter — translating one shape into another. If the library itself is fine to use directly, but painfully complicated, and you just want one simple entry point hiding its dozen options behind three sensible defaults, that is Facade — simplifying without translating. And if the library already works exactly the way you want, but you would like every sent email to also be automatically logged for record-keeping, that is Decorator — adding a new capability on top, without changing what sending an email fundamentally means. Same starting material, three genuinely different underlying problems, three genuinely different patterns.
Pros and Cons
A genuine trade-off. Weighing both sides fairly prevents Adapter from being reached for purely out of habit — or accumulating quietly into its own kind of tangled complexity.
Like every pattern, Adapter is a genuine trade-off. Weighing both sides fairly prevents it from being reached for purely out of habit.
Strengths
- Lets incompatible pieces of code cooperate without editing either one.
- Keeps legacy or third-party code completely untouched and low-risk.
- Makes swapping an underlying implementation considerably easier later.
- Keeps translation logic in one clean, testable, isolated place.
Trade-offs
- Adds an extra layer of indirection between client and adaptee.
- Can accumulate over time if used to patch over deeper design problems.
- A poorly written adapter can silently hide meaningful differences in behaviour.
The general guidance most experienced architects settle on: Adapter earns its place whenever two pieces of code genuinely cannot, or genuinely should not, be edited directly to match each other. Where both sides are fully within your control and easy to change, adjusting one of them directly is often simpler than introducing a permanent translation layer.
The “accumulates over time” trade-off deserves particular attention, since it is the one most likely to surprise a team years down the line rather than showing up immediately. A single adapter, written to bridge one clean piece of new code to one old, trusted class, is a genuinely healthy, low-risk decision. A codebase quietly filled with dozens of adapters, each bridging slightly different mismatches between systems that were never properly reconciled, becomes its own kind of tangled complexity — a maze of translation layers that can eventually become just as hard to reason about as the original mismatches they were meant to resolve. Periodically revisiting whether an old adapter can finally be retired, once its underlying legacy system is no longer needed, is a healthy habit worth building into any long-lived project.
Common Pitfalls
Four recurring traps — patching over deeper design problems, swallowing errors silently, stacking adapters on adapters, and skipping tests on “too simple to break” translations.
Using it to patch over a deeper design problem
If your own codebase keeps needing new adapters for classes you actually control, that is often a sign the underlying interfaces themselves should be redesigned, rather than endlessly patched over with more translation layers.
Letting an adapter silently swallow errors
A careless adapter that catches and hides errors from the Adaptee, rather than passing them through honestly, can leave the calling code with an incomplete or misleading picture of what actually went wrong.
Stacking adapters on top of adapters
Wrapping an adapter with another adapter, which is wrapped by yet another, can quietly create a confusing chain that is difficult to trace or reason about — a sign the overall interface strategy may need a more deliberate redesign.
Forgetting to test the translation itself
It is tempting to assume an adapter is “too simple to break,” and skip writing tests for it. But a small mistake in how one method’s arguments get translated into another’s can produce subtle, hard-to-trace bugs, especially when the original and translated meanings of a value do not line up as cleanly as first assumed — a distance measured in miles being passed straight through to a method expecting kilometres, for instance.
An adapter whose translation logic has grown far beyond simple method-call translation, quietly containing real business rules of its own. That is a sign the adapter has taken on responsibilities it was never meant to hold.
Frequently Asked Questions
Five honest questions that come up over and over — and honest answers to each.
Is Adapter the same as a “Wrapper”?
Yes — as mentioned earlier, “Wrapper” is a widely used alternate name for the exact same pattern, referring to how the adapter wraps around the original, incompatible class.
Can one adapter handle more than one incompatible class at once?
Typically, an adapter is built to translate one specific Adaptee’s interface. If several different, unrelated classes need adapting to the same Target interface, it is usually cleaner to write one small adapter per class, rather than one large adapter trying to handle several unrelated translations at once.
Does using Adapter always mean the original code is bad?
Not at all. The original class might be perfectly well-designed for its own original purpose — it simply was not designed with your specific, newer use case in mind. Adapter exists precisely for that mismatch, not as a judgment on the quality of the original code.
How is Adapter different from simply writing a helper function?
A helper function can achieve something similar for a single method call, but Adapter specifically provides an entire matching interface, letting the wrapped object be used anywhere the original interface is expected — including being passed into code that has no idea an adapter is even involved.
Should I write an adapter even if I only need it once?
If there is genuinely only one call site, and no reasonable chance of needing the same translation again elsewhere, a smaller, local fix might be simpler than a full adapter class. Adapter earns its structure once the same translation is needed in more than one place, or is likely to be reused later.
Key Takeaways
Wrap an incompatible class in a new, matching outer layer. Change the shape of how you talk to it, not what it does. Let both sides keep their own integrity intact.
Remember this
- The Adapter pattern converts an incompatible class’s interface into one your code already expects, without editing either side.
- It is built from four roles: Target, Client, Adaptee, and the Adapter itself, which bridges the two.
- Object adapters use composition and are the dominant modern approach; class adapters use multiple inheritance and are far less common today.
- It shows up constantly in legacy system integration, third-party API wrapping, data format conversion, and UI library migration.
- Adapter, Facade, and Decorator are often confused, but each wraps something for a genuinely different reason — fitting, simplifying, or enhancing.
- Its main risk is being used to patch over deeper design problems instead of addressing them directly.
- It earns its place whenever two pieces of code genuinely cannot, or should not, be edited directly to match one another.