When should you introduce an Adapter at a module or system boundary rather than changing the interface directly, and what are the design trade-offs?
answer
- Adapter controls dependency direction (depend on YOUR interface)
- Drivers: can't modify, stable callers, pluggable providers, anti-corruption layer
- Design the target from consumer needs, not the adaptee's shape
- Costs: extra layer, translation, impedance mismatch, leaky/anemic risk
- Object adapter at scale = Dependency Inversion at the boundary
basics
~20 sUse an Adapter when you can't or shouldn't change either side — third-party libraries, legacy code, or stable public APIs — to keep your code depending on your own clean interface instead of the foreign one. The cost is an extra layer to maintain and translate.
solid answer
~50 sIntroduce an Adapter at a boundary when you want to depend on an interface you control rather than a foreign or volatile one. Typical drivers: a third-party SDK you can't modify, legacy code with many existing callers, multiple interchangeable providers behind one abstraction, or an anti-corruption layer keeping an external model from leaking into your domain. The adapter implements your target interface and translates to the external API, so swapping vendors or upgrading the library touches only the adapter, and your core stays testable against a clean seam. The trade-offs: an extra layer to write and maintain, translation cost and possible impedance mismatch (foreign concepts that don't map cleanly), risk of a leaky or anemic abstraction if the target just mirrors the adaptee, and indirection that can obscure behavior. If you own both sides and the interfaces are stable, redesigning the interface directly is often cleaner than adding adapters.
go deeper
Understands an adapter wraps code you can't change so your program can still use it.
Identifies common reasons to adapt (third-party/legacy/swappable providers) and that the adapter localizes change.
Designs the target interface from consumer needs, recognizes impedance mismatch and leaky-abstraction risks, and isolates third-party churn behind one adapter per provider.
Frames Adapter as dependency-direction control and an anti-corruption layer, weighs adapter proliferation vs direct redesign, and sets team conventions for boundaries, testing seams, and provider swappability.
## The decision: adapt or change directly An Adapter is fundamentally about **dependency direction and ownership**. The core question at a boundary is: *whose interface does my code depend on?* If your code depends directly on a foreign type, every change to that foreign type ripples into you. An adapter inverts this: your code depends on **your** target interface, and a single adapter class absorbs the foreign API. ### When an Adapter earns its keep 1. **You can't modify the adaptee** — third-party library, OS API, generated client, another team's service. You have no choice but to wrap. 2. **Many callers depend on a stable interface** — changing the interface directly would break a large blast radius; an adapter lets new and old coexist. 3. **Pluggable providers** — payment gateways, message brokers, cloud SDKs. One `PaymentGateway` target with per-vendor adapters lets you swap or A/B providers without touching call sites. 4. **Anti-corruption layer (DDD)** — an external bounded context has a different, often messier model. The adapter translates external concepts into your clean domain model so the foreign model never **leaks** inward. 5. **Testing seam** — depending on your own narrow interface lets you substitute a fake in tests instead of mocking a sprawling third-party type. 6. **API/version isolation** — a library upgrade with breaking changes is contained to the adapter; the rest of the codebase is insulated. ### When to change the interface directly instead - You **own both sides** and the interfaces are stable — adding an adapter is then ceremony with no isolation benefit. - The adapter would be a **pass-through** that simply re-exposes the adaptee (anemic adapter) — it adds indirection without value. - The translation is so lossy that the adapter hides important semantics; sometimes callers genuinely need the richer foreign API. ## Trade-offs to weigh | Benefit | Cost | |---|---| | Isolates volatility/3rd-party churn | One more layer to build and maintain | | Clean, testable seam (own interface) | Translation logic can be error-prone | | Vendor/provider swappability | Impedance mismatch: foreign concepts may not map cleanly | | Protects domain model (anti-corruption) | Indirection can obscure where work happens | | Honors open/closed at the boundary | Risk of leaky/anemic abstraction if target mirrors adaptee | ## Design guidance - **Design the target interface from the consumer's needs**, not from the adaptee's shape. If you reverse-engineer the target from the foreign API, you import its warts (a leaky abstraction) and lose most of the benefit. - **Keep the adapter thin but meaningful** — it should translate, not just rename. If it does nothing but forward, question whether you need it. - **One adapter per provider** behind a shared target keeps each translation focused and the set open for extension. - **Beware impedance mismatch** — error models, threading, pagination, and nullability often differ; decide deliberately how to map them, and don't silently swallow foreign error semantics. - **Performance** — adapters add a hop and sometimes data copies; usually negligible, but on hot paths measure (e.g. a view-style adapter that avoids copying vs one that converts collections). ## In Java terms This is the object-adapter form (composition) applied at scale: an interface you own, a class per external dependency implementing it, dependency-injected so the rest of the app codes against the abstraction. It is the practical realization of the Dependency Inversion Principle at module boundaries — high-level code depends on an abstraction, and the adapter binds that abstraction to a concrete external detail.
- What is an 'anti-corruption layer' and how does Adapter relate to it?An anti-corruption layer (from DDD) sits between your domain and an external system, translating the external model into your own so foreign concepts don't leak into your code. Adapters are its primary building block — each adapter converts external types/operations into your clean domain interface.
- How can an Adapter become a liability rather than an asset?If the target interface is just reverse-engineered from the adaptee, the adapter is a leaky or anemic pass-through that adds indirection without isolation. It then imports the foreign API's warts and gives you maintenance cost with none of the decoupling benefit.
saying these in an interview costs you the question
- Adding an adapter when you fully own both sides and the interface is stable — that's needless indirection.
- Designing the target interface to mirror the adaptee, producing a leaky abstraction.
- Treating the adapter purely as renaming, ignoring error/threading/semantic impedance mismatch.
- Assuming an adapter is always free; on hot paths the extra hop and data copies can matter.