Free lesson · Low Level Design Fundamentals
Adapter
Adapter
The Adapter pattern converts one interface into another. The intent is to make classes that wouldn't otherwise work together cooperate by providing a translation layer between their interfaces. Adapter is one of the most-used structural patterns because real systems constantly integrate with code that has the wrong shape - legacy libraries, third-party APIs, generated code, code from other teams with different conventions.
The pattern is also one of the simplest in the GoF catalog. The classical structure is just "wrap an instance of class A in a class that implements interface B, translating the calls." Despite its simplicity, applying it correctly requires judgment about when to adapt versus when to change the underlying code, and what kind of adapter (class vs object) to use.
In interviews, Adapter often appears when the problem involves integration - "how would you integrate this third-party library" or "we're migrating from system X to system Y." Naming Adapter and explaining the translation strategy is a senior-shaped response.
The Problem It Solves
Consider integrating with a third-party payment library. Your application uses a standard interface for payment processing:
public interface PaymentProcessor {
PaymentResult charge(BigDecimal amount, PaymentMethod method);
PaymentResult refund(String transactionId, BigDecimal amount);
}
The third-party library uses a different interface - maybe older, maybe just different conventions:
public class LegacyPaymentLibrary {
public LegacyResponse processPayment(long centsAmount, String cardNumber, String cvv) {
// ...
}
public LegacyResponse processRefund(String referenceCode, long centsAmount) {
// ...
}
}
public class LegacyResponse {
public int statusCode;
public String responseMessage;
public String transactionReference;
}
The mismatches:
- Amount is
BigDecimalin our code,long(cents) in the library. - Payment method is
PaymentMethodin our code, separatecardNumberandcvvstrings in the library. - Method names differ (
chargevsprocessPayment). - Return types differ (
PaymentResultvsLegacyResponse). - Refund uses
transactionIdin our code,referenceCodein the library.
Without Adapter, we'd have to choose:
- Change our code to match the library. Now our standard interface depends on the library's conventions. If we switch libraries, much of our code changes.
- Change the library. Usually impossible - it's third-party.
- Pollute our business logic with translation. Each caller does the conversion inline. Code is duplicated and fragile.
Adapter provides a fourth option: wrap the library in a class that implements our interface, doing the translation in one place.
public class LegacyPaymentAdapter implements PaymentProcessor {
private final LegacyPaymentLibrary library;
public LegacyPaymentAdapter(LegacyPaymentLibrary library) {
this.library = library;
}
@Override
public PaymentResult charge(BigDecimal amount, PaymentMethod method) {
long cents = amount.multiply(new BigDecimal("100")).longValueExact();
LegacyResponse resp = library.processPayment(cents, method.cardNumber(), method.cvv());
return convertResponse(resp);
}
@Override
public PaymentResult refund(String transactionId, BigDecimal amount) {
long cents = amount.multiply(new BigDecimal("100")).longValueExact();
LegacyResponse resp = library.processRefund(transactionId, cents);
return convertResponse(resp);
}
private PaymentResult convertResponse(LegacyResponse resp) {
if (resp.statusCode == 200) {
return PaymentResult.success(resp.transactionReference);
}
return PaymentResult.failure(resp.responseMessage);
}
}
Now our business logic works against PaymentProcessor. The library's quirks are isolated in LegacyPaymentAdapter. Switching libraries means writing a new adapter; the business logic doesn't change.
The Structure
graph TD
A[Client] --> B[Target interface]
C[Adapter] --> B
C --> D[Adaptee]
E[Our PaymentProcessor] -.-> B
F[LegacyPaymentAdapter] -.-> C
G[LegacyPaymentLibrary] -.-> D
A diagram of the Adapter pattern. The Client uses the Target interface. The Adapter implements the Target interface and holds a reference to the Adaptee. When the Client calls a method on the Target, the Adapter translates the call into one or more calls on the Adaptee.
Four roles:
- Target. The interface the Client expects to use.
- Adapter. Implements Target by delegating to Adaptee, with translation.
- Adaptee. The existing class with the incompatible interface.
- Client. Code that uses the Target interface.
The Adapter is the bridge. From the Client's perspective, it's just another implementation of Target. From the Adaptee's perspective, it's just another client calling its methods. The translation happens entirely in the Adapter.
Class Adapter vs Object Adapter
GoF distinguishes two adapter variants:
Object Adapter (Composition-Based)
The Adapter contains an instance of the Adaptee:
public class LegacyPaymentAdapter implements PaymentProcessor {
private final LegacyPaymentLibrary library; // composition
public LegacyPaymentAdapter(LegacyPaymentLibrary library) {
this.library = library;
}
@Override
public PaymentResult charge(BigDecimal amount, PaymentMethod method) {
// translate and delegate
}
}
This is the form shown in the previous example. The Adapter holds the Adaptee as a field. Most modern Java Adapters use this form.
Class Adapter (Inheritance-Based)
The Adapter extends the Adaptee and implements the Target:
public class LegacyPaymentAdapter extends LegacyPaymentLibrary implements PaymentProcessor {
@Override
public PaymentResult charge(BigDecimal amount, PaymentMethod method) {
long cents = amount.multiply(new BigDecimal("100")).longValueExact();
LegacyResponse resp = processPayment(cents, method.cardNumber(), method.cvv());
// ... translate
}
}
The Adapter inherits the Adaptee's methods directly and implements the Target. No need to hold a separate Adaptee instance.
In Java, class adapters have practical problems:
- Single inheritance constraint. The Adapter can only extend one Adaptee. If you need to adapt two classes, class adapter doesn't work.
- Tight coupling. The Adapter is tied to the specific Adaptee class. Substituting different Adaptee implementations isn't possible without changing the Adapter.
- Inheritance issues. All the problems with inheritance from Module 5 apply.
For these reasons, object adapters are the standard form in Java. Class adapters appear occasionally but are usually the wrong choice.
In languages with multiple inheritance (C++) or mixins (Ruby, Scala traits), class adapters are more viable. The GoF book covers both because it was written when multiple inheritance was a more active possibility.
A Concrete Example - Stream Adapters
Java's I/O library has a classical adapter example:
// Adaptee - older API working with bytes
public class InputStream {
public int read() throws IOException { /* ... */ }
}
// Target - newer API working with characters
public class Reader {
public int read() throws IOException { /* returns chars */ }
}
// Adapter
public class InputStreamReader extends Reader {
private final InputStream stream;
private final CharsetDecoder decoder;
public InputStreamReader(InputStream stream, Charset charset) {
this.stream = stream;
this.decoder = charset.newDecoder();
}
@Override
public int read() throws IOException {
// Read bytes from stream, decode to chars
}
}
InputStream is the Adaptee (byte-oriented). Reader is the Target (character-oriented). InputStreamReader adapts. Clients that need character-oriented input can use any byte-oriented input source through the adapter.
This pattern appears throughout Java I/O - OutputStreamWriter adapts the opposite direction, Channels.newReader adapts NIO channels to Readers, etc. The library is largely built on adapters between byte and character abstractions.
Two-Way Adapters
Sometimes you need adaptation in both directions. The Adapter then implements both interfaces:
public interface ModernNotifier {
void notify(Notification notification);
}
public interface LegacyAlerter {
void sendAlert(String message, int priority);
}
public class TwoWayNotificationAdapter implements ModernNotifier, LegacyAlerter {
private final ModernNotifier modern;
private final LegacyAlerter legacy;
public TwoWayNotificationAdapter(ModernNotifier modern, LegacyAlerter legacy) {
this.modern = modern;
this.legacy = legacy;
}
@Override
public void notify(Notification notification) {
// Modern call - dispatch to whichever underlying impl makes sense
modern.notify(notification);
}
@Override
public void sendAlert(String message, int priority) {
// Legacy call - convert to modern notification
Notification n = new Notification(message, severityFromPriority(priority));
modern.notify(n);
}
}
Two-way adapters are useful when migrating between interfaces gradually. Old code calls sendAlert; new code calls notify; both work through the same adapter.
A Worked Example - Database API Migration
A common real-world adapter case: migrating from one database client library to another.
// Our application's standard interface
public interface OrderRepository {
Optional<Order> findById(String id);
List<Order> findByCustomer(String customerId);
void save(Order order);
void delete(String id);
}
// Old library - JDBC-based
public class JdbcOrderRepository implements OrderRepository {
private final JdbcTemplate jdbc;
public JdbcOrderRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public Optional<Order> findById(String id) {
// JDBC queries
}
// ...
}
// New library - JPA-based, very different API
public class JpaOrderEntity {
@Id private String id;
@Column private String customerId;
// ...
}
public class JpaOrderRepositoryImpl implements OrderRepository {
private final EntityManager entityManager;
public JpaOrderRepositoryImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Optional<Order> findById(String id) {
JpaOrderEntity entity = entityManager.find(JpaOrderEntity.class, id);
return Optional.ofNullable(entity).map(this::toOrder);
}
@Override
public List<Order> findByCustomer(String customerId) {
TypedQuery<JpaOrderEntity> query = entityManager.createQuery(
"SELECT o FROM JpaOrderEntity o WHERE o.customerId = :customerId",
JpaOrderEntity.class
);
query.setParameter("customerId", customerId);
return query.getResultList().stream()
.map(this::toOrder)
.toList();
}
@Override
public void save(Order order) {
JpaOrderEntity entity = toEntity(order);
entityManager.merge(entity);
}
@Override
public void delete(String id) {
JpaOrderEntity entity = entityManager.find(JpaOrderEntity.class, id);
if (entity != null) {
entityManager.remove(entity);
}
}
private Order toOrder(JpaOrderEntity entity) {
return new Order(entity.getId(), entity.getCustomerId(), /* ... */);
}
private JpaOrderEntity toEntity(Order order) {
JpaOrderEntity entity = new JpaOrderEntity();
entity.setId(order.id());
// ...
return entity;
}
}
The application's business logic uses OrderRepository throughout. The JDBC implementation works against one library; the JPA implementation against another. Migration: switch which concrete implementation is wired in the composition root. The business code doesn't change.
This is Adapter combined with DIP (Module 12). The repository interface (Target) lives in the domain. The implementations (Adapters) live in infrastructure. Each adapter handles the translation between domain concepts and library specifics.
When to Use Adapter
Adapter is the right pattern when:
Tip: Use Adapter when:
- You have existing classes with incompatible interfaces that you want to use together.
- The classes can't be modified (third-party libraries, generated code, legacy systems).
- You want to integrate without polluting the rest of the code with translation logic.
- You're migrating from one API to another and want to keep callers stable during the transition.
- You're testing existing code by adapting it to a test-friendly interface.
Adapter is overkill or wrong when:
- You control both sides. Just change the interface; no adapter needed.
- The translation is trivial. A few lines of inline conversion at the call site may be simpler than a whole adapter class.
- The interfaces are similar enough. Sometimes "adapting" is so simple it doesn't warrant a separate class.
- The translation is genuinely complex semantic mapping. Adapter is for interface mismatches, not for translating meaning. If you're computing something derived rather than just renaming, you may need a different abstraction.
Adapter and DIP
Adapter dovetails naturally with DIP (Module 12). The pattern often appears as the implementation of a DIP-respecting interface that abstracts over external code:
- Domain interface (Target): Owned by the application, expresses what the app needs.
- Adapter: Implements the domain interface using a specific external library.
- External library (Adaptee): Third-party or infrastructure code that the app shouldn't depend on directly.
The Adapter is the only piece of the application that knows about the specific library. Replacing libraries is replacing one adapter.
This is the Anti-Corruption Layer pattern from Domain-Driven Design - adapters that prevent external code from "corrupting" the domain's vocabulary. The adapter translates between domain concepts and external API concepts, keeping the domain pure.
Common Variations
Variant 1 - Plug-In Adapter
When an application supports multiple interchangeable adapters:
public interface PaymentProcessor {
PaymentResult charge(BigDecimal amount, PaymentMethod method);
}
public class StripeAdapter implements PaymentProcessor { /* ... */ }
public class PayPalAdapter implements PaymentProcessor { /* ... */ }
public class SquareAdapter implements PaymentProcessor { /* ... */ }
// Selected at runtime or by configuration
public class PaymentService {
private final PaymentProcessor processor;
public PaymentService(PaymentProcessor processor) {
this.processor = processor;
}
}
Each adapter wraps a different payment provider. The application can swap them via configuration. This is Strategy combined with Adapter - multiple strategies, each implemented as an adapter to a different external system.
Variant 2 - Adapter Chains
Sometimes adapters chain - one adapter wraps another to compose translations:
// Modern application interface
OrderRepository repo = new ModernRepositoryAdapter(
new MidLevelAdapter(
new LegacyDatabaseClient()
)
);
The Legacy client gets adapted to a mid-level interface, which then gets adapted to the modern interface. Each step bridges a layer of incompatibility.
Chains usually indicate that the code is in the middle of a migration. Once migration completes, the chain collapses to a single adapter.
Variant 3 - Default Method Adapters
Java 8+ default methods on interfaces enable a lightweight form of adapter:
public interface StorageBackend {
byte[] read(String key);
void write(String key, byte[] data);
// Default method adapts a common operation
default String readAsString(String key) {
return new String(read(key), StandardCharsets.UTF_8);
}
default void writeString(String key, String value) {
write(key, value.getBytes(StandardCharsets.UTF_8));
}
}
Default methods adapt between fundamental operations and convenient variants without requiring a separate adapter class. Not exactly the GoF pattern, but solves similar problems.
Real-World Examples
Adapter patterns are everywhere:
Arrays.asList() adapts an array to the List interface. The List is fixed-size (you can't add/remove) but supports List operations like iteration and indexing.
Collections.list() adapts an Enumeration (legacy iteration interface) to a List.
InputStreamReader and OutputStreamWriter adapt byte streams to character streams.
JPA EntityManager.find() - adapts between database rows and Java objects. The ORM itself is an enormous adapter between the relational model and the object model.
JDBC ResultSet - adapts database-specific result formats to a uniform Java API.
Hibernate, Spring Data, MyBatis - entire frameworks built around adapting database storage to Java domain objects.
Spring's HandlerInterceptor - adapts between HTTP request processing and various concerns.
Servlet container adapters - wrapping the application's servlets in container-specific adapter classes.
When you see a class whose primary job is translation between APIs, you're seeing the Adapter pattern.
Why This Matters in Interviews
Interview: Adapter is one of the most asked-about structural patterns:
Direct probes:
- "What's the Adapter pattern?" - Testing knowledge.
- "Class adapter or object adapter?" - Testing understanding of the variants.
- "How does Adapter differ from Decorator?" - Testing pattern discrimination.
Design problem probes: Integration questions almost always involve Adapter:
- "How would you integrate with this third-party library?"
- "How would you migrate from System A to System B without breaking callers?"
- "How would you make this legacy class work with our standard interface?"
Strong candidates name Adapter and discuss the translation strategy.
Common follow-up probes:
- "What if the libraries have very different semantics?" - Testing whether you recognize Adapter's limits.
- "Where would the adapter live in your architecture?" - Testing DIP connection.
- "How would you test the adapter?" - Testing testing-strategy awareness.
A strong articulation: "I'll use Adapter. The third-party payment library has a different interface from our internal PaymentProcessor - different method names, different parameter types, different response shape. I'll create a LegacyPaymentAdapter that implements our PaymentProcessor interface and wraps the library. The translation logic (BigDecimal to cents, our PaymentMethod to their card details) lives in the adapter. The rest of our code keeps using PaymentProcessor - if we switch providers, we write a new adapter. Test the adapter by mocking the third-party library; test the business logic by mocking PaymentProcessor."
Common Mistakes
Warning: Adapter failures:
Class adapter when object adapter would be cleaner. Multiple inheritance issues, tight coupling. Object adapter (composition) is almost always better.
Adapter for trivial differences. Sometimes "adapting" is just a method-name change. Just rename the method, don't introduce a class.
Pollution of the adapter. Adapters should focus on translation. If yours has business logic, it's doing too much.
Wrong Adapter direction. Sometimes the adapter wraps the wrong side. If you control the Target and not the Adaptee, adapt Adaptee to Target. If you control Adaptee, just change it.
Adapter chains that never collapse. Migration adapters that stay forever, accumulating layers. Once migration completes, collapse the chain to a single adapter.
Forgetting validation and error translation. The adapter must handle the Adaptee's error modes, often translating them to the Target's expected exceptions or result types.
Performance overhead from adapter. Each adapter call adds method invocation overhead. Usually negligible, but for performance-critical code with deeply nested adapters, it can matter.
Try It
Exercise: For each scenario, design the adapter:
Integrating a third-party JSON parser that uses a different API than your standard parser interface.
Migrating from
JdbcTemplate-based repositories to JPA, keeping the sameRepositoryinterface for callers.Using an old
Vector<Item>API in code that expectsList<Item>.Wrapping a synchronous library so it presents an asynchronous
CompletableFuture-based interface.Adapting a SOAP-based API to a REST-like JSON interface internally.
Two-way migration between an old
Customerclass and a newCustomerrecord, used by overlapping parts of the codebase.For each:
- Define the Target interface.
- Identify the Adaptee.
- Sketch the Adapter.
- Decide where the translation logic lives.
- Identify error/exception translation needs.
- Consider how the Adapter would be tested.
Then look at code you've worked on. Find a place that integrates with external code. Is there an explicit Adapter? Is the translation isolated, or is it leaking throughout the codebase? What would a clean Adapter look like?