Adapter Pattern
Convert an incompatible interface into one the client expects. A foundational structural pattern — the bridge between legacy code and modern systems, the glue that makes mismatched APIs work together.
Why Adapter Exists
You are integrating a third-party payment gateway. Your codebase processes payments through a clean internal interface: PaymentProcessor.charge(amount, currency). The new gateway — a vendor SDK you cannot modify — has a completely different API: VendorGateway.makePayment(centAmount, currencyCode, merchantId, callbackUrl). The method names differ, the parameter shapes differ (dollars vs. cents, String vs. Currency enum), and the vendor requires extra fields your interface doesn’t know about.
What goes wrong:
- Interface mismatch — your system expects
charge(double, String)but the vendor providesmakePayment(int, String, String, String); the shapes are incompatible - Tight coupling to vendor code —
OrderServicedepends directly onVendorGateway; swapping vendors means rewriting every call site - Conversion logic scattered — dollars-to-cents, currency mapping, and merchant ID are hard-coded wherever the vendor is called
- Untestable — you can’t substitute a mock because the client depends on the concrete vendor class, not on your own interface
This is the problem Adapter solves — create a wrapper class that implements your expected interface (PaymentProcessor) and internally translates calls to the vendor’s incompatible API. The client talks to your interface; the adapter handles the conversion; the vendor SDK is isolated behind a wall.
What Is Adapter?
Adapter is a structural pattern that converts the interface of an existing class into the interface the client expects. The adapter wraps the incompatible object (called the adaptee) and translates calls from the client’s language into the adaptee’s language. The client works exclusively with the target interface — it never knows the adaptee exists.
— Gamma, Helm, Johnson, Vlissides (1994)
Key insight: Adapter is the simplest structural pattern — it’s just a wrapper that translates one interface to another. It doesn’t add features (that’s Decorator), it doesn’t simplify a subsystem (that’s Facade), and it doesn’t control access (that’s Proxy). It does one thing: make an incompatible thing compatible. This is why it’s the go-to pattern when integrating third-party libraries, legacy systems, or vendor SDKs you cannot modify.
target.charge(amount, currency) β Adapter translates to adaptee.makePayment(cents, code, ...)Participants & Structure
| Participant | Role | In the Analogy |
|---|---|---|
| Target | The interface the client expects. Defines the domain-specific methods the client calls. The adapter implements this. | The US two-prong socket shape — what the charger expects. |
| Adaptee | The existing class with an incompatible interface. Often a third-party library, legacy system, or vendor SDK you cannot modify. | The European wall socket — it works, but the shape doesn’t match. |
| Adapter | Implements the Target interface and wraps (holds a reference to) the Adaptee. Translates Target method calls into Adaptee method calls, converting parameters, return types, and exceptions as needed. | The travel plug adapter — US prongs on one side, European pins on the other. |
| Client | Collaborates with objects through the Target interface. Doesn’t know about the Adapter or Adaptee — it just calls Target methods. | Your laptop charger — plugs into the adapter and gets electricity, never knowing it’s in Europe. |
- The diagram above shows an Object Adapter — the adapter wraps (composes) the adaptee via a field.
- This is the preferred approach in Java because it uses composition and the adapter can wrap any subclass of the adaptee.
- A Class Adapter uses multiple inheritance (the adapter extends both the Target and Adaptee).
- Java doesn’t support multiple class inheritance, so class adapters are rare — you’d need the target to be an interface and the adaptee to be a class, then extend the adaptee and implement the target.
The Pattern In Motion
Scenario: Your OrderService needs to charge $49.99 USD. It calls the Target interface. The StripeAdapter translates the call to the Stripe vendor SDK’s incompatible API.
processor.charge(49.99, "USD")OrderService) holds a PaymentProcessor reference. It doesn’t know the concrete type is StripeAdapter.StripeAdapter.charge(49.99, "USD") converts $49.99 β 4999 cents, maps "USD" β currency code, injects merchant ID from config.gateway.makePayment(4999, "USD", "MERCH_001", callbackUrl)VendorResult(txId, status). The adapter maps it to the Target’s return type: PaymentReceipt(txId, amount, currency).PayPalAdapter implements PaymentProcessorStripeAdapter. OrderService unchanged — zero edits.- You typically use Adapter when you have existing code on both sides that you can’t change — the client already depends on the Target interface, and the vendor/legacy system already exists.
- Adapter is the middleman you insert after the fact to make two independently-designed systems compatible.
- If you’re designing both sides from scratch, you don’t need Adapter — just design matching interfaces.
You Already Use This
Adapter is one of the most common patterns in the JDK. Whenever you see a class that converts one interface to another — especially in the I/O package — you’re looking at an adapter.
java.util.Arrays.asList(T[]) — adapts a raw array (Adaptee) to the List interface (Target). The returned list wraps the array; the client works with List methods while the underlying data structure remains a plain array. java.io.InputStreamReader(InputStream) — adapts a byte stream (InputStream, Adaptee) to a character stream (Reader, Target). The client reads chars; the adapter internally reads bytes and decodes them using a charset. This is the textbook Adapter: two incompatible I/O interfaces made compatible. java.io.OutputStreamWriter(OutputStream) — the reverse: adapts a byte OutputStream to a character Writer. Client calls write(String); adapter encodes to bytes and delegates to OutputStream.write(byte[]). java.io package is Adapter-heavy: InputStreamReaderadapts bytes to chars.BufferedReader(Reader)adds buffering (that’s actually Decorator, not Adapter).- The difference:
InputStreamReaderconverts one interface to another (Adapter);BufferedReaderadds behavior to the same interface (Decorator). - Both wrap, but for different purposes.
Build It Once
Domain: Payment Gateway Integration. A PaymentProcessor target interface, a StripeGateway vendor SDK (adaptee with incompatible API), and a StripeAdapter that bridges them.
- Notice how
StripeAdaptertranslates dollarsβcents whilePayPalAdapterbuilds a Map payload. - Each adapter handles its vendor’s quirks independently.
- The
OrderServiceclient is identical in both cases — it only knowsPaymentProcessor.charge(). - This is the power of Adapter: each vendor’s weirdness is isolated in its own adapter class.
Common Mistakes
- Adapter makes one incompatible interface match another.
- Facade simplifies a complex subsystem behind a new, simpler interface.
- If you’re wrapping 5 classes into one simple call, that’s Facade, not Adapter.
- If you’re making one existing class match an interface the client already expects, that’s Adapter.
- Adapter exists because you can’t modify at least one side (vendor SDK, legacy code, third-party library).
- If you own both the client and the service and they don’t match, just fix the interface.
- Don’t create an unnecessary adapter layer when a refactor is the right answer.
- If the adaptee throws
VendorPaymentExceptionbut the target interface declaresthrows PaymentFailedException, the adapter must catch the vendor exception and wrap it in the expected one. - Forgetting this leaks the vendor’s exception type into your codebase, defeating the purpose of the adapter.
When To Use Adapter
- You need to use an existing class whose interface doesn’t match what you need — vendor SDK, legacy system, third-party library
- You want to create a reusable class that cooperates with unrelated or unforeseen classes (classes with incompatible interfaces)
- You need to integrate multiple vendors behind one common interface — each gets its own adapter
- You can’t modify the adaptee — it’s closed source, a compiled JAR, or owned by another team
- You want to unit-test your client by mocking the target interface instead of the real vendor SDK
- You control both sides — just change the interface directly, no adapter needed
- You need to simplify a complex subsystem with many classes — use Facade instead
- You want to add behaviour to an existing interface — use Decorator instead
- You want to control access to an object — use Proxy instead
| Pattern | Purpose | Wraps? | When to pick it |
|---|---|---|---|
| Adapter ← this | Convert interface A to interface B | Yes (composition) | Incompatible interface you can’t modify |
| Decorator | Add behaviour to same interface | Yes (same interface) | Extend functionality without subclassing |
| Facade | Simplify complex subsystem | Wraps many objects | Too many classes to interact with |
| Proxy | Control access to object | Yes (same interface) | Lazy loading, access control, caching |
| Bridge | Separate abstraction from impl | Delegates to impl | Both sides vary independently (planned upfront) |
Problems To Solve
Adapter problems test whether you can identify the Target, Adaptee, and Adapter roles, translate parameters and return types correctly, and keep the adapter thin.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | Legacy Logger Adapter Your app uses a modern Logger interface with info(String), warn(String), error(String). A legacy library you can’t modify uses OldLogger.writeLog(int level, String msg) where level is 1=info, 2=warn, 3=error. Write an adapter so the legacy library can be used through your modern interface. | Tests the most basic adapter: map method names and translate a type-safe enum/method approach to a numeric-level approach. The adapter maps info() β writeLog(1, msg), warn() β writeLog(2, msg), etc. One method per target method, one line each. |
| Medium | Multi-Vendor Notification Adapter Your system sends notifications via NotificationService.send(String recipient, String message). Integrate 3 vendors you can’t modify: Twilio (sendSms(phoneNumber, body, fromNumber)), SendGrid (sendEmail(to, subject, htmlBody)), and Firebase (pushNotification(deviceToken, Map<String,String> data)). Write one adapter per vendor. | Tests multiple adapters implementing one Target. Each adapter translates differently: TwilioAdapter extracts phone from recipient and injects a fromNumber; SendGridAdapter splits message into subject/body; FirebaseAdapter builds a Map. The client code is identical regardless of which adapter is injected. |
| Medium | XML-to-JSON Adapter Your analytics system accepts data via a DataIngester.ingest(JsonObject data) interface. A legacy data source produces XML strings via XmlExporter.export() : String. Write an adapter that fetches XML from the exporter, parses it to JSON, and feeds it to the ingester’s expected interface. | Tests adapting not just method signatures but data formats. The adapter does structural translation (XML β JSON), not just parameter renaming. Demonstrates that adaptation can involve non-trivial transformation logic while still being “translation only” (no business rules). |
| Hard | Two-Way Adapter (Bidirectional) System A expects interface MediaPlayer.play(filename). System B expects interface AudioDevice.stream(url, format). Build a two-way adapter that implements both interfaces so either system can use it. When System A calls play("song.mp3"), it delegates to System B’s streaming API. When System B calls stream(url, format), it delegates to System A’s file-based API. | Tests the rare two-way adapter β a class that implements both Target interfaces simultaneously. The adapter holds references to both concrete implementations and translates in both directions. This is uncommon but appears in integration layers where two legacy systems must communicate bidirectionally through a bridge. |
- When asked about Adapter, the interviewer wants to see: (1) a clearly defined Target interface the client expects; (2) an Adaptee you explicitly state cannot be modified; (3) an Adapter that implements Target, wraps Adaptee, and only translates β no business logic; (4) awareness that Java’s
InputStreamReaderandArrays.asList()are real-world adapters. - Stand-out answers distinguish Adapter from Decorator (same interface + added behaviour), Facade (simplifies multiple classes), and Bridge (planned separation vs.
- Adapter’s after-the-fact fix).