Loading episodes…
0:00 0:00

The Adapter Pattern: Your Bridge for Incompatible Interfaces (Visually Explained)

00:00
BACK TO HOME

The Adapter Pattern: Your Bridge for Incompatible Interfaces (Visually Explained)

10xTeam November 18, 2025 10 min read

Have you ever found yourself with two pieces of code that should work together, but can’t because their interfaces are completely different? It’s like having a European plug and trying to fit it into an American wall socket. You don’t throw away your device; you use an adapter.

The Adapter pattern is a structural design pattern that does exactly that for your code. It allows objects with incompatible interfaces to collaborate.

The Core Problem: Incompatible Interfaces

Let’s visualize the classic analogy. You’re traveling with your laptop, which has a two-prong charger. But the country you’ve arrived in uses three-prong wall outlets. These two are incompatible. The solution isn’t to re-engineer the wall or your laptop; it’s to use a simple adapter that bridges the gap.

graph TD;
    A[Laptop Charger (2-Prong)] -- Incompatible with --> C[Wall Socket (3-Prong)];
    A -- Plugs into --> B(Power Adapter);
    B -- Plugs into --> C;
    style B fill:#f9f,stroke:#333,stroke-width:2px

In software, this happens all the time. Imagine your application relies on a data provider that returns data in XML format. Your app is perfectly happy with this. Now, you want to integrate a new, powerful analytics library, but it only understands JSON.

graph TD;
    subgraph Your Application
        A[XML Data Provider]
    end
    subgraph Third-Party
        C[Analytics Library (Expects JSON)]
    end
    A -- "XML Data" --> B(Adapter);
    B -- "Converts XML to JSON" --> C;
    style B fill:#f9f,stroke:#333,stroke-width:2px

The Adapter acts as a translator, converting the XML output into the JSON format the library needs, allowing them to work together without modifying either the provider or the library.

A Practical Example: Payment Gateways

Let’s build a real-world scenario. We have a checkout service in our e-commerce application. Initially, we only supported PayPal. Our system is built around a clean IPaymentProcessor interface.

Here’s our initial project structure:

src/
└── com/
    └── example/
        ├── payment/
        │   ├── IPaymentProcessor.java
        │   ├── PayPalProcessor.java
        │   └── CheckoutService.java
        └── Main.java

The IPaymentProcessor interface is simple. It expects a pay method that returns a boolean indicating success or failure.

// src/com/example/payment/IPaymentProcessor.java
package com.example.payment;

import java.math.BigDecimal;

/**
 * The target interface our application uses.
 */
public interface IPaymentProcessor {
    boolean pay(BigDecimal amount, String currency);
}

Our PayPalProcessor implements this interface directly.

// src/com/example/payment/PayPalProcessor.java
package com.example.payment;

import java.math.BigDecimal;

/**
 * A concrete implementation for a specific payment gateway.
 */
public class PayPalProcessor implements IPaymentProcessor {
    @Override
    public boolean pay(BigDecimal amount, String currency) {
        // Simulate calling PayPal's API
        System.out.println("Processing payment of " + currency + " " + amount + " via PayPal...");
        // In a real app, this would be an HTTP call that returns true/false
        return true;
    }
}

And our CheckoutService depends on the IPaymentProcessor interface, not on any concrete implementation. This is good design!

// src/com/example/payment/CheckoutService.java
package com.example.payment;

import java.math.BigDecimal;

public class CheckoutService {
    private final IPaymentProcessor paymentProcessor;

    public CheckoutService(IPaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void processCheckout(BigDecimal amount, String currency) {
        if (paymentProcessor.pay(amount, currency)) {
            System.out.println("Payment Succeeded!");
        } else {
            System.out.println("Payment Failed.");
        }
    }
}

The New Requirement: Adding Stripe

The business now wants to add Stripe as a payment option. We get the documentation for their library and immediately see a problem. The Stripe service has a completely different interface.

  • It has a method called makePayment.
  • It requires a specific StripeRequest object containing the amount, currency, and a description.
  • It returns a StripeResponse object where success is indicated by a status field being equal to "approved".

This is our incompatible interface.

// This is the third-party library we can't change.
package com.stripe.api;

// Request Object
public class StripeRequest {
    public double paymentAmount;
    public String currencyCode;
    public String description;
    // constructor, getters, setters...
}

// Response Object
public class StripeResponse {
    public String status; // e.g., "approved", "denied"
    // constructor, getters, setters...
}

// The Incompatible Service (Adaptee)
public class StripeService {
    public StripeResponse makePayment(StripeRequest request) {
        System.out.println("Processing payment via Stripe: " + request.description);
        StripeResponse response = new StripeResponse();
        response.status = "approved";
        return response;
    }
}

How do we integrate this without rewriting our CheckoutService?

[!WARNING] A common but poor solution would be to add if/else logic directly into the CheckoutService to handle Stripe differently. This violates the Open/Closed Principle and makes the code brittle and hard to maintain. if (processor instanceof Stripe) { ... } else if (processor instanceof PayPal) { ... }

The Solution: The Stripe Adapter

We’ll create an Adapter. The StripeAdapter will implement our application’s IPaymentProcessor interface but will internally delegate the call to the StripeService. It acts as the middleman, translating the request and the response.

First, let’s organize our files for the new adapter.

src/
└── com/
    └── example/
        ├── payment/
        │   ├── IPaymentProcessor.java
        │   ├── PayPalProcessor.java
        │   └── CheckoutService.java
        ├── adapters/
        │   └── StripeAdapter.java
        └── Main.java

Now, let’s build the StripeAdapter.

// src/com/example/adapters/StripeAdapter.java
package com.example.adapters;

import com.example.payment.IPaymentProcessor;
import com.stripe.api.StripeRequest;
import com.stripe.api.StripeResponse;
import com.stripe.api.StripeService;

import java.math.BigDecimal;

/**
 * The Adapter class. It implements our target interface and wraps the
 * incompatible object (the Adaptee).
 */
public class StripeAdapter implements IPaymentProcessor {
    private final StripeService stripeService;

    public StripeAdapter() {
        // The adaptee is the incompatible service we want to use.
        this.stripeService = new StripeService();
    }

    @Override
    public boolean pay(BigDecimal amount, String currency) {
        // 1. Translate the request from our interface to the adaptee's format.
        StripeRequest stripeRequest = new StripeRequest();
        stripeRequest.paymentAmount = amount.doubleValue();
        stripeRequest.currencyCode = currency;
        stripeRequest.description = "Checkout from Adapter Demo";

        // 2. Delegate the call to the adaptee.
        StripeResponse stripeResponse = stripeService.makePayment(stripeRequest);

        // 3. Translate the response from the adaptee's format to our interface's format.
        return "approved".equalsIgnoreCase(stripeResponse.status);
    }
}

The adapter performs three key steps:

  1. Translate Request: It converts the BigDecimal amount and String currency into a StripeRequest object.
  2. Delegate Call: It calls the makePayment method on the wrapped StripeService instance.
  3. Translate Response: It inspects the returned StripeResponse and translates its status field into the boolean our IPaymentProcessor interface expects.

Updating the Application Logic

Now, our main application logic can decide which processor to use without knowing the internal details. This is often handled by a Factory or a Dependency Injection container.

Here’s how we can modify our Main class to select the payment gateway.

--- a/src/com/example/Main.java
+++ b/src/com/example/Main.java
@@ -1,17 +1,24 @@
 package com.example;
 
 import com.example.payment.CheckoutService;
 import com.example.payment.IPaymentProcessor;
 import com.example.payment.PayPalProcessor;
+import com.example.adapters.StripeAdapter;
 
 import java.math.BigDecimal;
 
 public class Main {
     public static void main(String[] args) {
         String gateway = "stripe"; // This could come from user input or config
         BigDecimal amount = new BigDecimal("199.99");
 
         IPaymentProcessor processor;
         if ("paypal".equalsIgnoreCase(gateway)) {
             processor = new PayPalProcessor();
+        } else if ("stripe".equalsIgnoreCase(gateway)) {
+            processor = new StripeAdapter();
         } else {
-            throw new IllegalArgumentException("Unknown gateway");
+            throw new IllegalArgumentException("Unsupported payment gateway: " + gateway);
         }
 
         CheckoutService checkout = new CheckoutService(processor);
         checkout.processCheckout(amount, "USD");
     }
 }

By running this, whether we choose “paypal” or “stripe”, the CheckoutService works perfectly without any changes. We’ve successfully integrated a new, incompatible service without breaking our existing code.

Visualizing the Final Class Structure

This Mermaid diagram shows the final relationships between all the components.

classDiagram
    direction LR
    class Client {
        +processCheckout(IPaymentProcessor processor)
    }
    class IPaymentProcessor {
        <<interface>>
        +pay(amount, currency) boolean
    }
    class PayPalProcessor {
        +pay(amount, currency) boolean
    }
    class StripeService {
        <<Adaptee>>
        +makePayment(StripeRequest) StripeResponse
    }
    class StripeAdapter {
        <<Adapter>>
        -StripeService stripeService
        +pay(amount, currency) boolean
    }

    Client ..> IPaymentProcessor : depends on
    IPaymentProcessor <|-- PayPalProcessor : implements
    IPaymentProcessor <|-- StripeAdapter : implements
    StripeAdapter ..> StripeService : uses/wraps

[!TIP] Best Practice: Dependency Injection Instead of using if/else blocks to create processors, use a Dependency Injection (DI) framework. A DI container can be configured to provide the correct IPaymentProcessor implementation (PayPalProcessor or StripeAdapter) based on configuration, making your code even cleaner.

When to Use the Adapter Pattern

  1. Integrating Third-Party Libraries: When you need to use a third-party class, but its interface doesn’t match the rest of your application’s code.
  2. Refactoring Legacy Code: When you have a legacy component you want to reuse with modern interfaces, you can write an adapter that wraps the legacy code.
  3. Unifying Multiple Interfaces: If you have several subclasses with slightly different functionality (e.g., different APIs for fetching data), you can create a set of adapters to make their interfaces consistent.

The Adapter pattern is an indispensable tool for building flexible and maintainable systems. It allows your application to evolve and integrate with new technologies without requiring painful and risky rewrites of your core logic.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?