Adapter
Purpose
Let code depend on an interface it chose, while the object doing the work has a different one.
The adapter absorbs the mismatch — signature, model, vocabulary, error style — so that neither
side has to change and neither side learns about the other.
The measure of a boundary adapter is the coupling it intentionally contains. Domain-facing ports
usually should not expose vendor DTOs or exceptions; a thin interoperability adapter between two
libraries may deliberately retain shared standard types. State the boundary goal instead of
assuming every adapter is an anti-corruption layer.
When it is the answer
A third-party or legacy type does the work and its interface is not
yours to change
→ Adapter. This is the default and by far the commonest case.
Two libraries must interoperate and neither knows the other
→ Adapter, owned by the code that composes them, not by either.
Your own port defines what the application needs; several
implementations exist behind it
→ Adapter per implementation (this is ports-and-adapters;
the GoF pattern is the per-implementation half).
A test needs a fake implementation of an external dependency
→ the port exists for this too; the fake is not an adapter but
it is enabled by the same seam.
When it is not
- You own both sides and can change them atomically. Direct refactoring may be cheaper. An
adapter can still be correct across independently released modules, during migration, or where
two intentionally distinct models must remain separate.
- The mapping is one-to-one with no translation or boundary policy. A passthrough may be
removable, but can still own version isolation, telemetry, authorization or replacement
authority. Name and test that reason; otherwise delete it.
- It contains business rules. Deciding, defaulting, validating against domain policy — that
is domain logic in the boundary layer. Move it inward and leave translation behind.
- Its purpose is simplifying a subsystem behind a coarse call. Consider Facade
(
gof-facade); collaborator count alone does not classify a wrapper or exclude adaptation.
- It adds behaviour while keeping the same interface. That is a Decorator
(
gof-decorator).
Modern Java expression
Examples use Java 17 language features; inspect target compiler/runtime, actual SDK/API versions
and compatibility obligations before implementation. No pattern choice authorizes an upgrade.
Single-method interface mismatch a lambda or method reference:
Runnable r = task::execute;
Comparator<Order> c = comparing(Order::total);
Interface gained a method a legacy a default method on the interface,
implementor cannot supply implemented in terms of the others
Data model mismatch a record per boundary type, plus a
mapper — never the vendor's type in
the domain (remote-facade-and-dto)
Foreign exception hierarchy translate at the adapter into your
own, preserving the cause
(java-exception-design)
Whole-implementation mismatch an object adapter: a final class
holding the adaptee in a field
Class adapters — extends Adaptee implements Target — spend Java's single inheritance slot and
expose inherited public API, so composition is usually easier to isolate and replace. Inheritance
remains useful when a framework requires subclass hooks or the adaptee cannot be delegated
without losing protected extension behavior. Treat that as tighter coupling, not as impossible.
An interface default is suitable only when existing operations can satisfy the new method's
contract for all affected implementations; inspect inherited-default conflicts and behavioral
compatibility rather than inventing a no-op to make compilation pass.
Decision rules
IF a vendor type, exception or enum appears above a domain-facing adapter
THEN decide whether consumers now depend on vendor semantics. Translate when the
port is meant to protect that boundary; shared standards or deliberately thin
interoperability layers may preserve types explicitly.
IF a domain-facing port promises failure isolation but exposes the adaptee's exception type
THEN translate to the promised failure contract while preserving diagnostic cause; consumers
must not need vendor-specific catches. Thin interoperability contracts may differ explicitly.
IF the adapter interprets, defaults or decides
THEN distinguish provider-specific protocol interpretation from domain policy. Keep required
translation/validation here; move business decisions that remain after provider replacement inward.
IF the target is a functional interface and the method signatures/checked failures are compatible
THEN a lambda or method reference may suffice, regardless of how many methods the adaptee has.
Prefer a named class when lifecycle, state, substantial mapping or diagnostics warrant it.
IF the port has exactly one implementation and no second is planned,
and the implementation is your own code
THEN check dependency direction, test isolation, release boundaries and migration needs before
calling the port speculative. Implementation count alone is not a deletion criterion.
IF the adaptee is not thread-safe
THEN wrapping alone adds no guarantee. State confinement, per-call ownership or synchronization
covering all accesses, including aliases outside the adapter; otherwise retain the restriction.
IF the adaptee is remote
THEN its contract must expose or document latency and partial failure. Transport
timeouts belong near the client; end-to-end deadlines, retry and fallback policy
may belong to the caller or resilience layer (gof-proxy, timeouts-and-deadlines).
Cross-cutting checks
- Concurrency. A stateless adapter is shareable only if its dependencies/protocol permit it. It does not confer thread
safety on the adaptee: wrapping a non-thread-safe client in a "service" changes nothing. If
the adapter adds state — a cache, a connection, a cursor — it now owns a concurrency
contract and must document or enforce it.
- Distribution. Adapters are where a remote dependency's failure vocabulary is turned into
yours, and where its schema compatibility is checked. Transport timeout ownership may be in
injected client configuration; the caller can supply an end-to-end deadline. Unknown enum or
field values from a newer peer must be handled deliberately rather than throwing deep inside
the domain (
rpc-and-api-contracts).
- Lifecycle. Define whether the client, response stream or cursor is borrowed or owned, who
closes it and how cancellation/interruption propagates. Do not close an injected shared client
per call or turn an interrupted wait into an ordinary retryable provider failure.
- Performance. Dispatch is often inlined and translation cost ranges from zero-copy views to
full graph allocation. Inspect large collection copies, encoding conversions and eager
traversal; translating a lazily loaded structure can turn one query into many
(
orm-behavioral-patterns). Measure the boundary rather than counting wrapper calls.
- Testing. The port is the seam that lets the application be tested without the dependency;
the adapter itself needs tests against an authoritative implementation or compatible sandbox,
because its content is assumptions about a foreign system. Where that cannot run on every
commit, combine deterministic mapping tests with scheduled/provider contract tests
(
java-test-doubles, java-testing-strategy).
Review checklist
References
- Decision and alternatives — object versus class
adapters, Adapter set against Facade, Decorator, Proxy and the anti-corruption layer, the
error-translation rules, how to tell a translator with business rules from a mechanical
adapter, and how to remove a passthrough safely. Read when classifying or deleting a wrapper.
- Worked example — a vendor payment SDK adapted to a domain
port: model translation, exception translation, timeout ownership, unknown-status handling
from a newer API version, and the test split between a fake for the application and a
contract test for the adapter. Read when implementing.
1---2name: gof-adapter3description: Adapter in modern Java: making an existing type usable through an interface it was not written for, and keeping a foreign model, vocabulary and failure mode from leaking inward. Covers object versus class adapters, why a lambda already adapts a single-method interface, the error-translation duty most adapters omit, when an adapter has quietly become a translator with business rules in it, and when a passthrough should be deleted. Use when integrating a vendor SDK or legacy type behind your own port, when two libraries must interoperate, when an adapter is proposed between types you own, when foreign exceptions or DTOs appear in domain code, or when reviewing a wrapper that renames methods and does nothing else. Does not cover the Kubernetes telemetry sidecar (adapter-sidecar-pattern), simplifying a subsystem you own (gof-facade), adding behaviour to the same interface (gof-decorator), controlling access to an object (gof-proxy), or layering rules in general (layering-and-boundaries).4---56# Adapter78## Purpose910Let code depend on an interface it chose, while the object doing the work has a different one.11The adapter absorbs the mismatch — signature, model, vocabulary, error style — so that neither12side has to change and neither side learns about the other.1314The measure of a boundary adapter is the coupling it intentionally contains. Domain-facing ports15usually should not expose vendor DTOs or exceptions; a thin interoperability adapter between two16libraries may deliberately retain shared standard types. State the boundary goal instead of17assuming every adapter is an anti-corruption layer.1819## When it is the answer2021```text22A third-party or legacy type does the work and its interface is not23yours to change24 → Adapter. This is the default and by far the commonest case.2526Two libraries must interoperate and neither knows the other27 → Adapter, owned by the code that composes them, not by either.2829Your own port defines what the application needs; several30implementations exist behind it31 → Adapter per implementation (this is ports-and-adapters;32 the GoF pattern is the per-implementation half).3334A test needs a fake implementation of an external dependency35 → the port exists for this too; the fake is not an adapter but36 it is enabled by the same seam.37```3839## When it is not4041- **You own both sides and can change them atomically.** Direct refactoring may be cheaper. An42 adapter can still be correct across independently released modules, during migration, or where43 two intentionally distinct models must remain separate.44- **The mapping is one-to-one with no translation or boundary policy.** A passthrough may be45 removable, but can still own version isolation, telemetry, authorization or replacement46 authority. Name and test that reason; otherwise delete it.47- **It contains business rules.** Deciding, defaulting, validating against domain policy — that48 is domain logic in the boundary layer. Move it inward and leave translation behind.49- **Its purpose is simplifying a subsystem behind a coarse call.** Consider Facade50 (`gof-facade`); collaborator count alone does not classify a wrapper or exclude adaptation.51- **It adds behaviour while keeping the same interface.** That is a Decorator52 (`gof-decorator`).5354## Modern Java expression5556Examples use Java 17 language features; inspect target compiler/runtime, actual SDK/API versions57and compatibility obligations before implementation. No pattern choice authorizes an upgrade.5859```text60Single-method interface mismatch a lambda or method reference:61 Runnable r = task::execute;62 Comparator<Order> c = comparing(Order::total);6364Interface gained a method a legacy a default method on the interface,65implementor cannot supply implemented in terms of the others6667Data model mismatch a record per boundary type, plus a68 mapper — never the vendor's type in69 the domain (remote-facade-and-dto)7071Foreign exception hierarchy translate at the adapter into your72 own, preserving the cause73 (java-exception-design)7475Whole-implementation mismatch an object adapter: a final class76 holding the adaptee in a field77```7879Class adapters — `extends Adaptee implements Target` — spend Java's single inheritance slot and80expose inherited public API, so composition is usually easier to isolate and replace. Inheritance81remains useful when a framework requires subclass hooks or the adaptee cannot be delegated82without losing protected extension behavior. Treat that as tighter coupling, not as impossible.83An interface default is suitable only when existing operations can satisfy the new method's84contract for all affected implementations; inspect inherited-default conflicts and behavioral85compatibility rather than inventing a no-op to make compilation pass.8687## Decision rules8889```text90IF a vendor type, exception or enum appears above a domain-facing adapter91THEN decide whether consumers now depend on vendor semantics. Translate when the92 port is meant to protect that boundary; shared standards or deliberately thin93 interoperability layers may preserve types explicitly.9495IF a domain-facing port promises failure isolation but exposes the adaptee's exception type96THEN translate to the promised failure contract while preserving diagnostic cause; consumers97 must not need vendor-specific catches. Thin interoperability contracts may differ explicitly.9899IF the adapter interprets, defaults or decides100THEN distinguish provider-specific protocol interpretation from domain policy. Keep required101 translation/validation here; move business decisions that remain after provider replacement inward.102103IF the target is a functional interface and the method signatures/checked failures are compatible104THEN a lambda or method reference may suffice, regardless of how many methods the adaptee has.105 Prefer a named class when lifecycle, state, substantial mapping or diagnostics warrant it.106107IF the port has exactly one implementation and no second is planned,108and the implementation is your own code109THEN check dependency direction, test isolation, release boundaries and migration needs before110 calling the port speculative. Implementation count alone is not a deletion criterion.111112IF the adaptee is not thread-safe113THEN wrapping alone adds no guarantee. State confinement, per-call ownership or synchronization114 covering all accesses, including aliases outside the adapter; otherwise retain the restriction.115116IF the adaptee is remote117THEN its contract must expose or document latency and partial failure. Transport118 timeouts belong near the client; end-to-end deadlines, retry and fallback policy119 may belong to the caller or resilience layer (gof-proxy, timeouts-and-deadlines).120```121122## Cross-cutting checks123124- **Concurrency.** A stateless adapter is shareable only if its dependencies/protocol permit it. It does not confer thread125 safety on the adaptee: wrapping a non-thread-safe client in a "service" changes nothing. If126 the adapter adds state — a cache, a connection, a cursor — it now owns a concurrency127 contract and must document or enforce it.128- **Distribution.** Adapters are where a remote dependency's failure vocabulary is turned into129 yours, and where its schema compatibility is checked. Transport timeout ownership may be in130 injected client configuration; the caller can supply an end-to-end deadline. Unknown enum or131 field values from a newer peer must be handled deliberately rather than throwing deep inside132 the domain (`rpc-and-api-contracts`).133- **Lifecycle.** Define whether the client, response stream or cursor is borrowed or owned, who134 closes it and how cancellation/interruption propagates. Do not close an injected shared client135 per call or turn an interrupted wait into an ordinary retryable provider failure.136- **Performance.** Dispatch is often inlined and translation cost ranges from zero-copy views to137 full graph allocation. Inspect large collection copies, encoding conversions and eager138 traversal; translating a lazily loaded structure can turn one query into many139 (`orm-behavioral-patterns`). Measure the boundary rather than counting wrapper calls.140- **Testing.** The port is the seam that lets the application be tested without the dependency;141 the adapter itself needs tests against an authoritative implementation or compatible sandbox,142 because its content is assumptions about a foreign system. Where that cannot run on every143 commit, combine deterministic mapping tests with scheduled/provider contract tests144 (`java-test-doubles`, `java-testing-strategy`).145146## Review checklist147148- [ ] Any foreign type, exception or enum crossing outward is an explicit compatibility choice149- [ ] Adaptee exceptions are translated with the original preserved as the cause150- [ ] The adapter contains no decisions that belong to the domain151- [ ] Composition is preferred; inheritance has a documented framework/extension constraint152- [ ] A functional-interface adaptation uses the smallest form that preserves its contract/lifecycle153- [ ] Remote transport timeouts are configured and end-to-end resilience ownership is explicit154- [ ] The adapter is covered by authoritative integration/contract evidence at an appropriate cadence155- [ ] A passthrough has an evidenced boundary, lifecycle or compatibility responsibility, or is removed safely156157## References158159- [Decision and alternatives](references/decision-and-alternatives.md) — object versus class160 adapters, Adapter set against Facade, Decorator, Proxy and the anti-corruption layer, the161 error-translation rules, how to tell a translator with business rules from a mechanical162 adapter, and how to remove a passthrough safely. Read when classifying or deleting a wrapper.163- [Worked example](references/worked-example.md) — a vendor payment SDK adapted to a domain164 port: model translation, exception translation, timeout ownership, unknown-status handling165 from a newer API version, and the test split between a fake for the application and a166 contract test for the adapter. Read when implementing.