Structural

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.

Category: Structural Difficulty: Foundational Interview: Tier 2 Confused with: Bridge
01
Section One Β· The Problem

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.

Naive approach — calling the vendor SDK directly
// βœ— Client code coupled to the vendor's incompatible API class OrderService { private VendorGateway vendor = new VendorGateway(); public void checkout(double amount, String currency) { // Convert dollars to cents, map currency, hard-code merchant ID... int cents = (int) (amount * 100); vendor.makePayment(cents, currency, "MERCH_001", "https://cb.example.com"); } } // βœ— Switching to a different vendor means rewriting OrderService // βœ— Can't test OrderService without a real VendorGateway instance // βœ— Every call site duplicates the conversion logic

What goes wrong:

  • Interface mismatch — your system expects charge(double, String) but the vendor provides makePayment(int, String, String, String); the shapes are incompatible
  • Tight coupling to vendor codeOrderService depends directly on VendorGateway; 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
Without Adapter — client coupled to incompatible vendor API
OrderService expects charge(amount, currency) ✗ incompatible VendorGateway makePayment(cents, code, merchant, cb) ✗ Different method names, different parameters ✗ Conversion logic ($ β†’ cents) duplicated at every call site ✗ Can’t swap vendors ✗ Can’t mock for testing

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.

02
Section Two Β· The Pattern

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.

GoF Intent: “Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.”
— Gamma, Helm, Johnson, Vlissides (1994)
Analogy — travel power plug adapter: Your laptop charger has a US two-prong plug. The hotel in Europe has a round-hole socket (Type C). You can’t modify either one — the charger is sealed, the wall socket is cemented. So you buy a plug adapter: US prongs on one side, European pins on the other. The charger plugs into the adapter; the adapter plugs into the wall. Electricity flows — the charger never knows it’s in Europe. This is exactly what the Adapter pattern does in code: your client (charger) talks to a target interface (US socket shape); the adapter translates to the adaptee’s interface (European socket shape).

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.

The Adapter implements the Target interface the client expects
The client depends only on its own interface — zero knowledge of the vendor SDK
The Adapter holds (wraps) the Adaptee — the incompatible vendor object
All conversion logic (dollars β†’ cents, parameter mapping) lives in exactly one place
Client calls target.charge(amount, currency) β†’ Adapter translates to adaptee.makePayment(cents, code, ...)
The adaptee thinks it received a native call; the client thinks it called its own interface. Neither side changes.
Swapping vendors means creating a new Adapter, not editing the client
Open/Closed Principle satisfied — new integrations = new adapter class, zero edits to existing code
03
Section Three Β· Anatomy

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.
Adapter — UML Class Diagram (Object Adapter)
«interface» PaymentProcessor + charge(amount, currency) StripeAdapter - gateway : VendorGateway + charge(amount, currency) VendorGateway + makePayment(cents, code, ...) cannot modify (vendor SDK) wraps Client uses ■ Blue = Target interface (what client expects) ■ Gold = Adapter (the translator) ■ Red = Adaptee (incompatible, can’t modify) ◆ filled diamond = composition --▷ dashed = implements / depends
Object Adapter vs. Class Adapter:
  • 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.
04
Section Four Β· How It Works

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.

Step 1 — Client calls Target: processor.charge(49.99, "USD")
The client (OrderService) holds a PaymentProcessor reference. It doesn’t know the concrete type is StripeAdapter.
Step 2 — Adapter receives the call and translates parameters
StripeAdapter.charge(49.99, "USD") converts $49.99 β†’ 4999 cents, maps "USD" β†’ currency code, injects merchant ID from config.
Step 3 — Adapter delegates to Adaptee: gateway.makePayment(4999, "USD", "MERCH_001", callbackUrl)
The vendor SDK receives a native-looking call with the exact parameters it expects. It processes the payment and returns a result.
Step 4 — Adapter translates the response back
The vendor returns VendorResult(txId, status). The adapter maps it to the Target’s return type: PaymentReceipt(txId, amount, currency).
Step 5 — Switching vendors: create PayPalAdapter implements PaymentProcessor
New adapter class wraps the PayPal SDK, translates differently. Inject it instead of StripeAdapter. OrderService unchanged — zero edits.
Adapter — Call Sequence
OrderService StripeAdapter VendorGateway charge(49.99, "USD") $ β†’ cents, inject config makePayment(4999, "USD", ...) VendorResult β†’ PaymentReceipt PaymentReceipt(txId, $49.99, USD)
The pattern in pseudocode
// ── Client uses only the Target interface ── PaymentProcessor processor = new StripeAdapter(new VendorGateway()); // Client calls Target method β€” doesn't know it's adapted PaymentReceipt receipt = processor.charge(49.99, "USD"); // Internally: adapter converts $49.99 β†’ 4999 cents // adapter calls gateway.makePayment(4999, "USD", ...) // adapter maps VendorResult β†’ PaymentReceipt // ── Switch vendors: new adapter, zero edits to client ── PaymentProcessor processor2 = new PayPalAdapter(new PayPalSDK()); PaymentReceipt receipt2 = processor2.charge(49.99, "USD");
Adapter is a “bridge to the past”:
  • 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.
05
Section Five Β· Java Stdlib

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.

IN JAVA
Example 1 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.
Example 2 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.
Example 3 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[]).
Stdlib usage — InputStreamReader as Adapter
// Adaptee: InputStream (reads bytes) InputStream byteStream = new FileInputStream("data.txt"); // Adapter: InputStreamReader (converts bytes β†’ chars) Reader charStream = new InputStreamReader(byteStream, StandardCharsets.UTF_8); // Client uses the Target interface (Reader) β€” doesn't know about bytes int ch; while ((ch = charStream.read()) != -1) { System.out.print((char) ch); // reads chars, not bytes }
Stdlib usage — Arrays.asList() as Adapter
// Adaptee: raw array String[] array = {"Alice", "Bob", "Charlie"}; // Adapter: Arrays.asList wraps the array in a List view List<String> list = Arrays.asList(array); // Client uses List interface β€” doesn't know it's backed by an array list.forEach(System.out::println); // Alice, Bob, Charlie list.set(0, "Zara"); // modifies the underlying array! // list.add("New") β†’ UnsupportedOperationException (fixed-size view)
The entire java.io package is Adapter-heavy:
  • InputStreamReader adapts bytes to chars.
  • BufferedReader(Reader) adds buffering (that’s actually Decorator, not Adapter).
  • The difference: InputStreamReader converts one interface to another (Adapter); BufferedReader adds behavior to the same interface (Decorator).
  • Both wrap, but for different purposes.
06
Section Six Β· Implementation

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.

Java — Adapter Pattern Payment Gateway (core)
// ── Target interface (what our system expects) ── interface PaymentProcessor { String charge(double amount, String currency); } // ── Adapter (translates Target β†’ Adaptee) ── class StripeAdapter implements PaymentProcessor { private final StripeGateway gateway; public StripeAdapter(StripeGateway gateway) { this.gateway = gateway; } @Override public String charge(double amount, String currency) { int cents = (int) Math.round(amount * 100); // translate return gateway.makePayment(cents, currency, "MERCH_001"); } }
Two adapters, one interface:
  • Notice how StripeAdapter translates dollarsβ†’cents while PayPalAdapter builds a Map payload.
  • Each adapter handles its vendor’s quirks independently.
  • The OrderService client is identical in both cases — it only knows PaymentProcessor.charge().
  • This is the power of Adapter: each vendor’s weirdness is isolated in its own adapter class.
07
Section Seven Β· Watch Out

Common Mistakes

Mistake #1 — Adapter that does too much: An adapter should only translate one interface to another. If your adapter contains business logic, caching, retries, or validation, it’s become a God class. Keep the adapter thin — translate parameters, delegate the call, translate the response. Put business logic in the client or a service layer, not in the adapter.
✗ Wrong — adapter contains business logic
// βœ— Adapter doing way more than translation class StripeAdapter implements PaymentProcessor { public String charge(double amount, String currency) { if (amount > 10000) throw new LimitException(); // ← business rule! applyDiscount(amount); // ← not translation! logTransaction(); // ← cross-cutting! return gateway.makePayment(...); } }
✔ Correct — adapter only translates
// βœ“ Adapter is thin β€” translate, delegate, return class StripeAdapter implements PaymentProcessor { public String charge(double amount, String currency) { int cents = (int) Math.round(amount * 100); // only param translation return gateway.makePayment(cents, currency, merchantId); } }
Mistake #2 — Confusing Adapter with Facade:
  • 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.
Mistake #3 — Adapting when you control both sides:
  • 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.
Mistake #4 — Not adapting exceptions:
  • If the adaptee throws VendorPaymentException but the target interface declares throws 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.
08
Section Eight Β· Decision Guide

When To Use Adapter

Use Adapter When
  • 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
Avoid Adapter When
  • 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
Adapter vs. Confused Patterns
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)
Decision Flowchart
Need to make two things work together? Yes Interface mismatch? (can’t modify adaptee) No Refactor / Facade Yes One class with wrong interface? No (many) Facade Yes Adapter
09
Section Nine Β· Practice

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.
Interview Tip:
  • 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 InputStreamReader and Arrays.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).