LESSON 5.1
Adapter
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that could not otherwise because of incompatible interfaces.
13 min read
Real-world analogy
When traveling from North America to the United Kingdom, your laptop charger plug has two flat pins designed for 120 V outlets, while the wall socket expects three rectangular pins at 230 V. You do not rewire the hotel wall or solder new prongs onto your charger; instead, you plug your charger into a travel adapter that translates the physical prongs and electrical characteristics.
The problem
You are building an e-commerce checkout pipeline. Your core domain defines an internal payment abstraction, PaymentProcessor, which accepts a customer identifier and a charge amount in whole dollars. You decide to integrate a third-party vendor SDK (StripeGateway). However, the vendor library exposes incompatible method names, requires amounts in integer cents, and expects an opaque card token rather than your internal customer ID.
// Naive approach: Direct coupling to vendor SDK in business logic
class OrderService {
#stripe;
constructor(stripe) {
this.#stripe = stripe; // Tight coupling to external vendor signature
}
checkout(customerId, amountDollars) {
const cents = Math.round(amountDollars * 100);
const token = `tok_${customerId}`;
// Directly invoking vendor API scatters vendor types throughout domain
return this.#stripe.executeCharge(cents, { cardToken: token });
}
}public class Example {
static class StripeGateway {
public String executeCharge(int amountInCents, String cardToken) {
return "stripe_ch_" + amountInCents;
}
}
static class OrderService {
private final StripeGateway stripe;
This naive approach leaks external vendor details across the business domain. If you add PayPal, switch payment processors, or write unit tests, every caller must be refactored.
The solution
The Adapter pattern converts the interface of an existing class (Adaptee) into another interface (Target) that clients expect, allowing classes with incompatible contracts to collaborate seamlessly.
- Target (
PaymentProcessor): Defines the domain-specific interface that client code depends on. - Client (
OrderService): Collaborates with objects conforming to the Target interface. - Adaptee (
StripeGateway): The existing or third-party class that has the needed functionality but an incompatible interface. - Adapter (
StripePaymentAdapter): Implements the Target interface and holds an instance of the Adaptee, translating calls and data representations.
Step-by-step implementation
Step 1: Define the target domain contract
Establish the interface your application expects. Callers depend exclusively on this abstraction.
class PaymentProcessor {
processPayment(customerId, amountDollars) {
throw new Error("processPayment() must be implemented");
}
}public class Example {
interface PaymentProcessor {
PaymentResult processPayment(String customerId, double amountDollars);
}
Step 2: Implement the adapter wrapping the adaptee
Create an adapter that implements the target interface and delegates to the encapsulated vendor SDK instance.
Step 3: Connect client to the adapter
The client relies only on PaymentProcessor. Injecting the adapter satisfies the contract without polluting business rules.
class OrderService {
#processor;
constructor(processor) {
this.#processor = processor;
}
completeOrder(customerId, total) {
return this.#processor.processPayment(customerId, total);
}
}The complete example
Here is the complete, runnable implementation showing the target interface, vendor adaptee, adapter, and client execution.
Order ORD-101: Paid via ch_stripe_4995_tok_usr_99
Order ORD-102: Paid via pp_usr_88@customer.internal_12.50Variations
Object adapter vs class adapter
There are two primary flavors of the Adapter pattern:
- Object Adapter (Composition): The adapter implements the target interface and holds a reference to an instance of the adaptee. Because it relies on composition, it works in virtually all object-oriented languages (including Java and JavaScript) and can adapt the adaptee as well as any of its subclasses.
- Class Adapter (Inheritance): The adapter inherits from both the target interface (or class) and the adaptee class simultaneously. This allows the adapter to override adaptee behavior directly without delegating through an internal instance. However, it requires multiple inheritance of classes (supported in C++, but not supported in Java or JavaScript).
Two-way adapters
A Two-Way Adapter implements both the Target interface and the Adaptee interface simultaneously. It provides transparent interoperability in bidirectional environments where Legacy System A needs to invoke Modern System B, and Modern System B simultaneously needs to invoke Legacy System A as if it were a native component.
Where you've already used it
java.io.InputStreamReader: Adapts a byte stream (InputStream) into a character stream (Reader), handling character encoding conversion internally.java.util.Arrays.asList(T...): Adapts a contiguous native array into a fixed-sizeList<T>contract.- Node.js
util.promisify: Adapts standard error-first callback functions into Promise-returning async functions. - Spring
HandlerAdapter: Adapts arbitrary controller handler methods (annotated methods, functional handlers) to the standardDispatcherServletrequest workflow. - React wrappers: Wrapping an imperative jQuery or Canvas widget inside a declarative React functional component with props and
useEffect.
Pros and cons
Advantages
- Single Responsibility: Separates interface and data conversion code from core business domain logic.
- Open/Closed Principle: Introduces new adapters without breaking existing clients or modifying third-party classes.
- Reusability: Allows legacy or vendor code to be reused in modern systems without invasive rewrites.
Disadvantages
- Increased Complexity: Introduces new interfaces and classes, increasing architectural surface area.
- Indirection Overhead: Adds an extra layer of method invocation and parameter transformation.
- Feature Impedance: If the adaptee lacks capabilities required by the target interface, the adapter must throw
UnsupportedOperationExceptionor synthesize fallback behavior.
When not to use it
Do not introduce an Adapter when you own both interfaces and can refactor them directly to agree on a common signature. Introducing an adapter between two internal classes that you can easily edit adds unnecessary boilerplate and indirection.
Related patterns
| Pattern | Purpose | Interface Relationship |
|---|---|---|
| Adapter | Converts one existing interface into another | Modifies/maps interface |
| Decorator | Adds dynamic behavior without subclassing | Retains identical interface |
| Facade | Simplifies access to a complex subsystem | Creates new, simplified interface |
| Proxy | Controls access, caching, or lazy loading | Retains identical interface |
| Bridge | Decouples abstraction from implementation upfront | Separates orthogonal hierarchies |
Adapter: payment gateway
Adapt a legacy third-party XML payment service to a modern JSON payment interface so checkout requires no legacy XML formatting.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
How does Adapter differ from Facade? Adapter bridges two existing incompatible interfaces, mapping one exact signature to another. Facade defines a new, simplified high-level interface over an entire subsystem without conforming to an existing legacy contract.
Can an Object Adapter adapt subclasses of the Adaptee? Yes. Because an Object Adapter holds a reference to the Adaptee by type composition, polymorphism allows any subclass of the Adaptee to be passed into the Adapter.
What is the Class Adapter pattern and why is it rare in Java and JavaScript? Class Adapter inherits from both the target and the adaptee. Because Java and JavaScript support single class inheritance only, Class Adapter cannot be implemented if both target and adaptee are concrete classes.
What should an adapter do if the target interface expects a capability the adaptee cannot provide?
The adapter can provide a sensible default, synthesize the value through auxiliary calls, or throw an explicit UnsupportedOperationException. Documenting this impedance mismatch is critical.
Try it
Logging Framework LLD
Compose structured logging, redaction and bounded asynchronous output.