Adapter Pattern
Concept
Adapter converts the interface a class actually exposes into the interface calling code expects, without modifying either side. The canonical trigger is integration: a third-party library, a legacy module, or an external API exposes methods shaped one way, and the code that needs to consume it was written (or needs to be written) against a different, cleaner interface — often because that interface also has to work with other implementations the adapted class knows nothing about. Rather than reshaping the calling code around the third-party shape (which leaks that dependency's design decisions throughout the codebase) or modifying the third-party class directly (often impossible — it's a vendored library or generated client), a thin wrapper class is introduced that implements the expected interface and internally translates each call into the adapted class's actual method names, parameter order, and data shapes.
There are two structural variants. Object adapter (composition) holds a reference to an instance of the adapted class and delegates to it — this is the form that works in any language, doesn't require multiple inheritance, and can adapt an instance handed to it at runtime rather than only a fixed class. Class adapter (inheritance) subclasses both the target interface and the adapted class simultaneously — only possible in languages with multiple inheritance (C++, not Java/C#/Python's single-inheritance model for classes, though Python's mixins get close), and less flexible since it commits to one specific adapted class at compile time rather than any object satisfying a shape. Modern practice defaults to the object adapter almost universally, both for the language-portability reason and because composition-based adapters are easier to test in isolation (mock the wrapped object, not a subclassing chain).
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| Adapter (wrap, translate calls) | Calling code depends only on the interface it expects; the third-party/legacy shape is fully contained in one file | An extra class per adapted type; every method on the target interface needs an explicit translation, which is tedious for wide interfaces |
| Modify calling code to match the external interface directly | No extra class | The external dependency's design leaks into every call site; swapping the dependency later means touching all of them instead of one adapter |
| Modify the adapted class itself | No wrapper needed | Usually impossible (vendored/generated code) or unsafe (a legacy class other code also depends on in its original shape) |
| Facade (a simplified interface over a subsystem, not a translation of one interface into another) | Reduces a complex multi-call interaction to one entry point | Solves a different problem — Facade simplifies, Adapter translates; conflating the two produces a class that neither simplifies nor cleanly translates |
Adapter is frequently confused with Facade because both interpose a class between calling code and something else, but the intent differs: Adapter exists because two interfaces are incompatible and need to interoperate; Facade exists because an interface is complex and needs to be simplified, even when nothing is incompatible.
When to use / when not to
- Use when integrating a third-party SDK, legacy module, or generated API client whose method names/signatures don't match an interface your own code already depends on elsewhere — a payments provider adapter, a cloud-storage adapter that lets S3/GCS/Azure Blob all satisfy one
ObjectStoreinterface. - Especially valuable when the same expected interface needs to work against multiple incompatible concrete implementations (four LLM provider SDKs, three cloud storage backends) — one adapter per implementation, one interface the rest of the app calls against, so adding a fifth provider means writing a new adapter, not touching call sites.
- Don't add an adapter for a dependency your code only calls in one place with no plan to swap or abstract it — that's an extra indirection layer with no consumer who benefits from it yet.
- Don't reach for Adapter when the actual need is simplifying a multi-step interaction over an already-compatible subsystem — that's Facade, and forcing it through an "adapter" produces a class with the wrong intent documented in its name.
Common pitfall
Writing an adapter that only translates the happy path — matching method signatures and return shapes — while silently dropping or mismapping how the adapted class's errors surface. A wrapped payment SDK that throws its own exception type, or returns a provider-specific error code, needs that failure mode translated into the target interface's error contract just as deliberately as its success path; skipping this means callers who correctly handle the target interface's documented errors still get blindsided by a raw exception type they've never seen, leaking the exact implementation detail the adapter existed to hide.
Engineering Lens
Adapter is one of the few GoF patterns whose value is almost entirely about where a dependency's design decisions live rather than any runtime behavior change — the wrapped call still does the same thing, just through a translation layer. In review, the useful question isn't "is this technically an Adapter" but "if this third-party API changes its interface, or a second provider needs to be supported, does exactly one file change, or does the change ripple through every call site that talks to it directly." A codebase that reaches for third-party SDKs directly at dozens of call sites has effectively distributed one vendor's interface decisions throughout the code without naming that as a dependency; introducing an adapter after the fact (rather than up front) is a legitimate refactor once a second implementation or a vendor swap is actually on the table — doing it preemptively for a dependency that will only ever have one implementation is speculative abstraction with no real payoff yet.
Related
- Factory Method and Abstract Factory — often paired with Adapter: a factory selects and constructs the right adapter for a given provider/config at runtime