
Your startup just acquired a payment vendor whose API prices oil in litres, while your entire order management system prices in gallons. The vendor’s library is closed source, tested, and already integrated in other teams. Rewriting it is not an option. Rewriting your codebase to speak litres would touch hundreds of call sites and risk rounding errors in every invoice. What you need is a thin translation layer that lets your gallon-based code call the litre-based library as if it had always spoken gallons.
That layer is the Adapter design pattern in Java. It is a structural pattern that converts the interface of an existing class into an interface the client expects, letting otherwise incompatible types collaborate without modification. Also known as Wrapper, it is the reason java.io.InputStreamReader can turn a byte stream into a character stream and why Arrays.asList() can make an array look like a List. If you have not seen how creational families are isolated behind factories, read Abstract Factory Pattern in Java: Explanation and Example for the contrast between creating new families and adapting existing ones.
Table of Contents
Open Table of Contents
- What Is the Adapter Pattern?
- Why Adapter Exists: The Compatibility Problem
- Participants: Target, Adaptee, and Adapter
- Class Adapter vs Object Adapter
- Mermaid Architecture Diagram
- Real-World Code Example: Gallons to Litres Oil Trading
- Real-World Example: How Slack, Spring, and JDK Use Adapters
- When to Use vs When NOT to Use
- Advantages and Trade-offs
- Common Pitfalls
- Interview Questions
- 1. What problem does the Adapter pattern solve and how does it differ from Decorator and Facade?
- 2. How do class adapter and object adapter differ, and which do you prefer in Java?
- 3. Where is Adapter used in the JDK and Spring?
- 4. What are legitimate alternatives to Adapter when interfaces do not match?
- 5. How do you test an Adapter thoroughly without testing the adaptee itself?
- 6. What risks does Adapter hide if used carelessly?
- Conclusion
- References
- YouTube Videos
What Is the Adapter Pattern?
The Adapter pattern lets two incompatible interfaces work together by inserting an intermediate object that implements the expected interface and delegates to the existing one. The existing interface is called the Adaptee, the interface the client expects is the Target, and the new bridging class is the Adapter. The client never touches the adaptee directly. It calls the target interface, the adapter translates arguments or return types, and the real work happens in the legacy implementation.
The everyday analogy is the mobile charger. A wall socket delivers 120V or 240V AC, but a phone battery requires 5V DC. You do not rewire the wall or redesign the battery. You plug a charger adapter in between that converts voltage and connector shape while preserving the function of delivering power. Software adapters behave identically: they convert units, naming conventions, data structures, or error models so a proven component can be reused.
Adapter is distinct from decorator or facade, and the distinction matters for exam and review conversations. Decorator adds new behavior to the same interface. Facade simplifies a set of interfaces into a smaller surface. Adapter changes one existing interface into another that the client already expects. If you are adding caching, you are probably decorating. If you are hiding ten services behind one clean method, you are facading. If you are making OldApi.buyOil(litres) satisfy NewApi.buyOilInGallon(gallons), you are adapting.
Why Adapter Exists: The Compatibility Problem
Compatibility problems are rarely hypothetical. Amazon’s retail platform must talk to carriers that each expose a different shipment contract: one carrier expects weight in kilograms and dimensions in centimeters, another in pounds and inches. FedEx, UPS, and regional carriers all describe the same domain differently. Rewriting your internal fulfillment service for each carrier would fracture domain logic. Writing an adapter per carrier lets the fulfillment service keep its canonical units and delegate to the carrier-specific API through translation.
The same story plays out inside a monolith that is slowly migrating from a legacy library. Perhaps your codebase used java.util.Date and now must expose java.time.Instant to new callers, or you have adopted a new analytics SDK whose event schema renamed userId to user_id. Without adapters, both the old and new contracts leak into business logic, which then carries two representations for one concept and bugs that only appear when a developer forgets which contract a method expects.
Adapter pays for itself because it keeps the change boundary thin. The translation between gallons and litres, between Date and Instant, between String userId and UUID user_id lives in one class that can be unit tested with exhaustive edge cases, including rounding, overflow, and null handling. Callers stay clean. The alternative is scattered conversion littered across dozens of service methods, where one missed conversion in an invoice path can cost real money.
Participants: Target, Adaptee, and Adapter
Target defines the interface the client expects. In our trading example, OilInGallonInterface with buyOilInGallon(double gallons) is the target. It is often a new domain interface that you own. WHY make it an interface rather than a concrete class? Because the client should depend on the smallest portable contract so tests can swap in a fake adapter without wiring a real trading library.
Adaptee is the existing interface that already does the work, but with the wrong shape: OilInLitreInterface with buyOil(double litres). You cannot or should not change this interface. WHY not change it? Perhaps it is a third-party library, a frozen legacy module, or a class shared by other teams whose tests would all break if you renamed a method. Adapter respects that stability boundary.
Adapter implements target and wraps adaptee. Its method translates arguments, calls the adaptee, and optionally translates the result or exception back to the target’s language. WHY implement the target instead of extending the adaptee? Because the client is typed against the target. The client should be able to declare OilInGallonInterface trader = new OilInGallonImplObject() and never name the litre class. That inversion of dependency direction is the entire structural benefit.
Class Adapter vs Object Adapter
There are two implementation flavors, and choosing between them is a common interview checkpoint. In Java, only one is idiomatic today.
Class adapter uses inheritance. The adapter extends the adaptee class and implements the target interface. The adapter method calls super.buyOil(convertedLitres). This looks concise, but it tightly couples the adapter to the adaptee’s inheritance chain. If OilInLitre is final, or if it already extends another class, Java’s single inheritance blocks the pattern. Class adapter also inherits every protected method and field, exposing unrelated members that pollute code completion and invite misuse.
Object adapter uses composition. The adapter holds an instance of OilInLitreInterface and delegates to it. This is the preferred approach because it respects composition over inheritance, works with final classes and interfaces, and keeps the adaptee’s surface hidden behind a private field. The adapter can wrap any implementation of the adaptee interface, including mocks, decorators, or retry proxies, without changing adapter code. Spring’s entire adapter ecosystem, from HandlerAdapter to AdvisorAdapter, follows this composition model for exactly that reason.
The practical consequence for modern Java is to default to object adapter. Reach for class adapter only when the adaptee is a lightweight class you control, the hierarchy is shallow, and overriding protected behavior is intentionally desired. Those conditions are rare in production code, which is why most codebases and every major framework treat object adapter as the canonical form. If you are building deep inheritance trees elsewhere, review whether composition would serve you better, as discussed in SOLID Principles in Java.
Mermaid Architecture Diagram
flowchart TD
C[Client\nbuys oil in gallons] --> T[Target Interface\nOilInGallonInterface]
T --> AO[Object Adapter\nOilInGallonImplObject]
T --> AC[Class Adapter\nOilInGallonImplClass]
AO --> AIF[Adaptee Interface\nOilInLitreInterface]
AC --> AEXT[Adaptee Class\nOilInLitre]
AIF --> A[Adaptee\nOilInLitre]
AEXT --> A
A --> OIL[Domain Object\nOil]
OIL --> RET[Return to Client]
RET --> C
classDef target fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000000;
classDef adapter fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000000;
classDef adaptee fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000000;
classDef client fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#000000;
class T target;
class AO,AC adapter;
class AIF,AEXT,A adaptee;
class C,RET,OIL client;
The flow shows the branching decision that matters at design time: object adapter delegating through the adaptee interface versus class adapter inheriting the class. Both converge on the same domain result, but object adapter keeps the adaptation boundary behind composition.
Real-World Code Example: Gallons to Litres Oil Trading
The client’s order system prices oil in gallons. A proven trading library prices oil in litres and already implements price calculation and purchase. The integration must not rewrite the library, but must expose gallon semantics to new caller code without leaking litre details upward.
Domain Object
package com.adevguide.java.designpatterns.adapter;
public class Oil {
private final double oilPrice; // WHY: final keeps purchased price immutable
public Oil(double quantityInLitre) {
// WHY: validate inputs at the domain boundary, not deep in adapter translation
if (quantityInLitre < 0) {
throw new IllegalArgumentException("Quantity cannot be negative");
}
// WHY: price is 2 dollars per litre - keep domain rule in one place so adapter translators share it
this.oilPrice = quantityInLitre * 2;
System.out.println("Total Cost of purchase is " + oilPrice + " dollars. Purchase Complete.");
}
public double getOilPrice() {
return oilPrice;
}
}
Adaptee Interfaces and Implementation
package com.adevguide.java.designpatterns.adapter;
// WHY: adaptee interface is what already exists and what we cannot break for other teams
public interface OilInLitreInterface {
Oil buyOil(double quantityInLitre);
}
package com.adevguide.java.designpatterns.adapter;
// WHY: adaptee implementation stays untouched; adapter will wrap it instead of forking it
public class OilInLitre implements OilInLitreInterface {
@Override
public Oil buyOil(double quantityInLitre) {
System.out.println("Purchasing " + quantityInLitre + " litres of Oil");
return new Oil(quantityInLitre);
}
}
Target Interface
package com.adevguide.java.designpatterns.adapter;
// WHY: target interface lives in the client's domain language so domain code never says litre
public interface OilInGallonInterface {
Oil buyOilInGallon(double quantityInGallon);
}
Class Adapter (Inheritance Form - Educational)
package com.adevguide.java.designpatterns.adapter;
// WHY: extends adaptee for legacy illustration, but prefer object adapter in production
public class OilInGallonImplClass extends OilInLitre implements OilInGallonInterface {
private static final double GALLON_TO_LITRE = 3.78541;
@Override
public Oil buyOilInGallon(double quantityInGallon) {
// WHY: translation is the only responsibility of adapter, so keep conversion private and pure
double quantityInLitres = convertGallonToLitre(quantityInGallon);
// WHY: delegate to inherited adaptee method so existing pricing logic is reused verbatim
return buyOil(quantityInLitres);
}
private double convertGallonToLitre(double gallonQuantity) {
if (gallonQuantity < 0) {
throw new IllegalArgumentException("Gallon quantity cannot be negative");
}
return gallonQuantity * GALLON_TO_LITRE;
}
}
Object Adapter (Composition Form - Preferred)
package com.adevguide.java.designpatterns.adapter;
public class OilInGallonImplObject implements OilInGallonInterface {
private static final double GALLON_TO_LITRE = 3.78541;
// WHY: composition lets us wrap any OilInLitreInterface, including a fake for tests or a retry proxy
private final OilInLitreInterface oilInLitre;
// WHY: inject adaptee via constructor for testability; default constructor wires the real implementation
public OilInGallonImplObject() {
this(new OilInLitre());
}
public OilInGallonImplObject(OilInLitreInterface oilInLitre) {
// WHY: requireNonNull prevents NullPointerException deep in buyOil; fail fast at construction
this.oilInLitre = java.util.Objects.requireNonNull(oilInLitre, "oilInLitre must not be null");
}
@Override
public Oil buyOilInGallon(double quantityInGallon) {
double quantityInLitres = convertGallonToLitre(quantityInGallon);
// WHY: delegate to composed adaptee instance so we never inherit unwanted members
return oilInLitre.buyOil(quantityInLitres);
}
private double convertGallonToLitre(double gallonQuantity) {
if (gallonQuantity < 0) {
throw new IllegalArgumentException("Gallon quantity cannot be negative");
}
return gallonQuantity * GALLON_TO_LITRE;
}
}
Client That Sees Only the Target Interface
package com.adevguide.java.designpatterns.adapter;
public class Client {
public static void main(String[] args) {
System.out.println("Adapter Class Implementation");
OilInGallonInterface adapterInterfaceClass = new OilInGallonImplClass();
adapterInterfaceClass.buyOilInGallon(1);
System.out.println("***************************************************************");
adapterInterfaceClass.buyOilInGallon(10);
System.out.println("***************************************************************");
System.out.println("Adapter Object Implementation");
OilInGallonInterface adapterInterfaceObject = new OilInGallonImplObject();
adapterInterfaceObject.buyOilInGallon(1);
System.out.println("***************************************************************");
adapterInterfaceObject.buyOilInGallon(40);
// WHY: the same client type talks to both adapters; interchangeable under the target interface
OilInGallonInterface testableAdapter = new OilInGallonImplObject(
litres -> {
System.out.println("[FAKE] Would purchase " + litres + " litres");
return new Oil(litres);
}
);
testableAdapter.buyOilInGallon(2);
}
}
Output:
Adapter Class Implementation
Purchasing 3.78541 litres of Oil
Total Cost of purchase is 7.57082 dollars. Purchase Complete.
***************************************************************
Purchasing 37.8541 litres of Oil
Total Cost of purchase is 75.7082 dollars. Purchase Complete.
***************************************************************
Adapter Object Implementation
Purchasing 3.78541 litres of Oil
Total Cost of purchase is 7.57082 dollars. Purchase Complete.
***************************************************************
Purchasing 151.4164 litres of Oil
Total Cost of purchase is 302.8328 dollars. Purchase Complete.
[FAKE] Would purchase 7.57082 litres
Total Cost of purchase is 15.14164 dollars. Purchase Complete.
The final lambda fake shows WHY object adapter wins in production. In a real service you would inject a fake adaptee that records litres without printing or charging, and you would assert that 2 gallons correctly becomes 7.57 litres before any financial logic runs.
Real-World Example: How Slack, Spring, and JDK Use Adapters
JDK I/O bridges. java.io.InputStreamReader adapts an InputStream (bytes) to a Reader (characters) and OutputStreamWriter adapts the reverse. The reason is historical layering: the byte stream API shipped early and the character API arrived later to handle encodings properly. Instead of rewriting every byte stream, the JDK introduced a one-class bridge that handles charset decoding and encoding while exposing the desired interface. Every Java developer who reads a text file through new BufferedReader(new InputStreamReader(inputStream, UTF_8)) is stacking an adapter inside a decorator without thinking about pattern names.
Spring MVC HandlerAdapter. Spring must dispatch requests to controllers that look completely different: a method annotated with @RequestMapping, a legacy Controller interface, an HttpRequestHandler. Spring does not force every handler to inherit one base type. Instead, a family of HandlerAdapter implementations each knows how to recognize its handler type and invoke it. The dispatcher iterates adapters and delegates to the first that supports the handler. This is the object adapter pattern powering a framework used by millions of applications, and it explains why adding a new handler type in Spring does not require changes to the dispatcher.
Slack Bolt framework and legacy notification services. Teams that integrate Slack, Microsoft Teams, and email often start with three separate notification clients that each accept a different payload shape. Slack expects blocks, Teams expects sections, email expects HTML. An adapter per channel that implements a single NotificationSender.send(Notification notification) lets the core domain code fire one method and remain ignorant of the external payload contract. When Slack updates its block schema, only SlackNotificationAdapter changes, which contains the blast radius and allows the change to be reviewed by whoever owns the Slack integration.
When to Use vs When NOT to Use
Use Adapter when:
-
A tested or frozen component already does what you need, but speaks a different type contract. Reuse beats rewrite when the component has years of hardening or is owned by another team.
-
You must integrate a third-party SDK that you cannot fork. Payment gateways, carriers, and analytics SDKs are textbook triggers.
-
The adaptation is a pure translation between contracts without new business rules. If the translation is lossless and narrow, the adapter stays thin and obviously correct.
Do NOT use Adapter when:
-
Both interfaces are under your control and can be aligned directly. If you own the adaptee, rename the method or unify the type rather than introducing an indirection that makes navigation harder.
-
You are adding behavior rather than translating an interface. Wrapping a list to add logging is decoration, not adaptation. Introduce a decorator instead so the intent is legible to reviewers.
-
The translation would hide semantic mismatches that callers must know about. Adapting a synchronous
buyOil()into an asynchronous reactive signature without exposing the latency and failure modes can mislead callers. When the semantics diverge deeply, formalize the boundary with an anti-corruption layer or a dedicated port-and-adapter hexagonal boundary rather than a silent name-level adapter.
| Dimension | Use Adapter | Prefer Something Else |
|---|---|---|
| Compatibility | Interfaces mismatch but semantics align | No mismatch, or semantics diverge deeply |
| Ownership | Adaptee is frozen or third-party | You own both sides and can unify |
| Scope | Narrow translation like units, naming, structure | Broad behavior addition or workflow simplification |
| Preferred form | Object adapter via composition | Class adapter only if hierarchy is trivial, or facade/decorator for different intent |
Advantages and Trade-offs
Adapter gives reuse with stability. Legacy code stays untouched, new code depends on an intentional interface that reflects the current domain language, and the translation of gallons to litres or bytes to characters is tested once and shared everywhere. This directly upholds Single Responsibility and Open-Closed health alongside the other principles in SOLID Principles in Java.
The trade-off is indirection and proliferation. Every adapted call routes through one more allocation, and a codebase that adapts eagerly ends up with UserAdapter, UserLegacyAdapter, UserV2Adapter, and the inevitable UserAdapterAdapter that someone introduced to bridge an adapter. Navigation and debugging cost rises because stack traces include the bridge frame and logs interleave target and adaptee messages. Reviewers also stop trusting names that end in Adapter when some of them secretly add retry or caching behavior. Use the name only when the class truly adapts an interface.
Performance also deserves explicit reasoning. The double dispatch and allocation cost is negligible for most business services, but in a hot loop that translates millions of events per second, an adapter that allocates a wrapper per event can show up in GC profiles. In that narrow case, you may co-locate the translation with the caller or reuse a stateless adapter instance. It is the only place where avoiding object allocation is a defensible argument against object adapter.
Common Pitfalls
Leaking adaptee methods through class adapter. When OilInGallonImplClass extends OilInLitre, code completion surfaces buyOil(double litres) alongside buyOilInGallon(double gallons) on the same object. A developer under deadline will call the litre method from gallon code and skip conversion, creating an invoice that is 3.78x wrong. The fix is to default to object adapter so the adaptee is private, or if you must use class adapter, at least override the adaptee method to throw or deprecate it so misuse fails loudly.
Swallowing exceptions or changing failure contracts without translation. The litre API might throw TradingLimitExceededException that the gallon domain maps to OrderRejectedException with a different handling policy. An adapter that catches the former and returns null or logs and swallows hides a financial signal. Always translate exceptions into the target’s error model explicitly and document whether the translation loses information. Where resource acquisition happens, wrap adaptation calls with try and release in finally or use try-with-resources so partial purchases do not leak connections.
Bi-directional adapters that try to handle both directions in one class. Teams sometimes build a reversible adapter that both converts gallons to litres and litres to gallons in the same object with two methods. That class soon accumulates bidirectional state, rounding branches, and flags that make it hard to test. Keep adapters unidirectional. A client that needs both directions depends on two adapters that each do one translation well, rather than one adapter that does two translations badly and invites circular wiring.
Interview Questions
1. What problem does the Adapter pattern solve and how does it differ from Decorator and Facade?
Adapter solves interface incompatibility: a client needs an interface that an existing class does not provide, so you wrap the existing class behind the expected interface. Decorator solves behavior augmentation: a client already has the right interface but wants extra features like logging or caching without changing the underlying object. Facade solves complexity: a client faces many interfaces and wants a single simplified entry point. The test in an interview is naming the intent precisely. If you are bridging List and Array views, that is adapter as in Arrays.asList(). If you are adding synchronization to a stream, that is decorator as in BufferedReader. If you are collapsing payment, inventory, and shipping calls into checkout(), that is facade.
2. How do class adapter and object adapter differ, and which do you prefer in Java?
Class adapter uses inheritance where the adapter extends the adaptee and implements the target. Object adapter uses composition where the adapter holds an adaptee instance and delegates. In Java, object adapter is strongly preferred because it avoids single-inheritance limits, works with final adaptees, supports any implementation of the adaptee interface including test doubles, and hides unrelated inherited members. Class adapter is only sensible when the adaptee is a small non-final class you own and overriding protected behavior is explicitly intended. Java frameworks such as Spring chose object adapter for HandlerAdapter precisely because handlers come from diverse lineages that cannot be forced into one hierarchy.
3. Where is Adapter used in the JDK and Spring?
In the JDK, InputStreamReader and OutputStreamWriter adapt byte streams to character streams, Arrays.asList() adapts arrays to lists, and Collections.enumeration() adapts collections to the legacy Enumeration. In Spring MVC, each HandlerAdapter knows how to invoke one style of handler while the dispatcher depends only on the adapter interface. The value of citing these in an interview is showing you recognize the wrapper naming signal. Whenever a Java API exposes a constructor that takes one interface and returns another, look for the adapter role. The pattern is so common in I/O and frameworks that most mid-level Java codebases contain several adapters even if no file is named *Adapter explicitly.
4. What are legitimate alternatives to Adapter when interfaces do not match?
If you control only one side, aligning the interfaces directly be removing the mismatch is cleaner than wrapping it forever. If the translation is not just naming but a domain concept translation with different lifecycles, model an explicit anti-corruption layer with its own value objects and invariants rather than a shadow translation inside an adapter. If you are adapting many members of a legacy API at once, a facade around the whole subsystem may communicate intent more clearly than a bag of individual adapters. The decision rule is translation narrowness. When the mismatch is a single method signature or type shape, adapter is precise. When the mismatch is systemic, a thicker boundary with domain translation deserves its own module.
5. How do you test an Adapter thoroughly without testing the adaptee itself?
Test the adapter at its boundaries: construction validation, argument translation, return translation, exception translation, and reuse of the wrapped instance. For the gallon adapter, verify that zero and negative inputs fail fast, that one gallon becomes exactly 3.78541 litres within floating point tolerance, that the delegated Oil result is returned unchanged, and that an exception from the adaptee is surfaced under the target’s error model if one exists. Mock the adaptee in unit tests so you can capture the litres passed to buyOil and assert the translation. Avoid end-to-end purchase tests in the adapter test class. Those belong to the adaptee’s own suite, which already covers price calculation correctness.
6. What risks does Adapter hide if used carelessly?
Adapter can hide semantic mismatches behind a clean signature. Converting gallons to litres looks mechanical, but if the adaptee mutates shared state, requires explicit resource cleanup, or advertises idempotency guarantees that the target domain violates, the adapter’s one-line delegation silently inherits those assumptions. The client then fails in production under concurrency or retries with a stack trace that points at the adapter boundary. Mitigate by documenting lifecycle and thread-safety expectations in the target interface, by keeping try/finally or try-with-resources handling inside the adaptation layer when resources are involved, and by writing at least one integration test that crosses the real adaptee rather than only a mock.
Conclusion
Adapter is valuable when an existing component is worth reusing and only its interface is wrong:
- Match intent to pattern. Choose adapter when the problem is interface mismatch, decorator when you add behavior, and facade when you simplify a subsystem.
- Prefer object adapter. Composition keeps the adaptee private, allows constructor injection of fakes, and avoids inheritance pollution that surfaces as leaked methods.
- Translate once, test thoroughly. Centralize unit conversion, naming, or structural mapping so edge cases like rounding and validation are exercised in one focused test class.
- Catch leaky semantics. Translate exceptions and lifecycle expectations, not just parameter names, and document what is preserved and what is lost.
- Keep adapters narrow. A one-direction, one-translation adapter stays understandable. A reversible, multi-concern adapter becomes a second codebase.
The next step in this series on structural patterns covers Composite – treating single objects and object trees uniformly – which complements Adapter because it harmonizes usage shape rather than bridging it. For a refresher on creation mechanics that often precede adaptation, revisit Factory Design Pattern Java Simplified and Factory Method Design Pattern Java Simple Detailed Examples.
References
- Adapter Pattern - Refactoring Guru
https://refactoring.guru/design-patterns/adapter - Adapter Pattern - Baeldung
https://www.baeldung.com/java-adapter-pattern - Design Patterns: Elements of Reusable Object-Oriented Software - Gamma et al.
https://en.wikipedia.org/wiki/Design_Patterns
YouTube Videos
-
“Adapter Design Pattern in Java Explained | Structural Design Patterns Tutorial”
https://www.youtube.com/watch?v=XDcHSCSM0e4 -
“Adapter Design Pattern in Java | Low Level Design Interview Questions”
https://www.youtube.com/watch?v=VrUbpLgd2fY -
“Adapter Design Pattern”
https://www.youtube.com/watch?v=tQCsFpKTaqg