Skip to main content

Command Palette

Search for a command to run...

Adapter Design Pattern

Updated
4 min read
Adapter Design Pattern

Adapter Design Pattern is a structural design pattern that converts the interface of an existing class into another interface that the client expects. It allows incompatible classes to work together without modifying either of them.

The main intention of this pattern is to wrap an existing class (Adaptee) inside a new class (Adapter) that implements the interface the client already understands (Target Interface).

The client continues to interact with the Target interface. The Adapter receives the request, translates it into the format expected by the Adaptee, and delegates the call. Because of this extra translation layer, neither the client code nor the existing class needs to change.

Why Do We Need It?

Suppose your application is built around a common payment interface:

public interface PaymentProcessor {
    boolean processPayment(PaymentRequest request);
}

All payment providers are expected to implement this interface.

Now imagine you want to integrate a third-party UPI library:

public class UPIGateway {

    public boolean makePayment(String upiId, double amountInPaise) {
        System.out.printf("UPI: ₹%.2f -> %s%n",
                amountInPaise / 100.0,
                upiId);
        return true;
    }

    public String getGatewayName() {
        return "UPI-V3";
    }
}

The problem is that the gateway does not implement PaymentProcessor.

It exposes a completely different API:

makePayment(String upiId, double amountInPaise)

while our application expects:

processPayment(PaymentRequest request)

Changing the application is risky, and modifying the third-party library is usually impossible.

This is where the Adapter Pattern helps.

Real-World Analogy

A common real-world example is a travel adapter.

A US plug cannot fit directly into a UK wall socket.

Instead of modifying the plug or rebuilding the wall socket, we use a travel adapter that sits in between and converts one interface into another.

US Plug --> Travel Adapter --> UK Socket

The Adapter Design Pattern follows exactly the same principle.

Client --> Adapter --> Third Party Library

Implementation

// Target Interface
public interface PaymentProcessor {
    boolean processPayment(PaymentRequest request);
}

public record PaymentRequest(
        String recipientId,
        double amount,
        String currency) {
}

Adaptee (Existing Class)

public class UPIGateway {

    public boolean makePayment(
            String upiId,
            double amountInPaise) {

        System.out.printf(
                "UPI: ₹%.2f -> %s%n",
                amountInPaise / 100.0,
                upiId);

        return true;
    }

    public String getGatewayName() {
        return "UPI-V3";
    }
}

Adapter

public class UPIPaymentAdapter
        implements PaymentProcessor {

    private final UPIGateway gateway;

    public UPIPaymentAdapter(UPIGateway gateway) {
        this.gateway = gateway;
    }

    @Override
    public boolean processPayment(
            PaymentRequest request) {

        String upiId =
                request.recipientId() + "@upi";

        double amountInPaise =
                request.amount() * 100;

        System.out.printf(
                "[%s] routing payment%n",
                gateway.getGatewayName());

        return gateway.makePayment(
                upiId,
                amountInPaise);
    }
}

Client

public class CheckoutService {

    private final PaymentProcessor processor;

    public CheckoutService(
            PaymentProcessor processor) {
        this.processor = processor;
    }

    public void checkout(
            String userId,
            double totalAmount) {

        PaymentRequest request =
                new PaymentRequest(
                        userId,
                        totalAmount,
                        "INR");

        boolean success =
                processor.processPayment(request);

        System.out.println(
                success
                        ? "Payment succeeded"
                        : "Payment failed");
    }
}

Usage

CheckoutService checkout =
        new CheckoutService(
                new UPIPaymentAdapter(
                        new UPIGateway()));

checkout.checkout("andrew", 499);

Output:

[UPI-V3] routing payment
UPI: ₹499.00 -> andrew@upi
Payment succeeded

Notice that CheckoutService knows only about PaymentProcessor.

It has no dependency on UPIGateway.

Class Adapter vs Object Adapter

There are two common ways to implement Adapter.

The adapter contains an instance of the adaptee.

Client
   |
Target Interface
   |
 Adapter
   |
 Adaptee

Advantages:

  • Follows composition over inheritance

  • More flexible

  • Can adapt multiple implementations

This is the approach used in most modern applications.

Class Adapter

The adapter inherits from the adaptee.

class PaymentAdapter
        extends UPIGateway
        implements PaymentProcessor {
}

Advantages:

  • Slightly simpler implementation

Drawbacks:

  • Client can directly access Adaptee methods

  • Tight coupling

  • Cannot adapt multiple classes because Java supports only single inheritance

Use this approach only when inheritance is genuinely required.

When Should You Use Adapter Pattern?

Use Adapter when:

  • Integrating third-party libraries.

  • Working with legacy systems.

  • Migrating from one service provider to another.

  • Existing interfaces are incompatible.

  • Modifying existing code is difficult or impossible.

Avoid it when:

  • Interfaces already match.

  • The additional abstraction provides no benefit.

  • A direct implementation is simpler and easier to maintain.

A good rule of thumb is:

If two components should collaborate but their interfaces do not match, introduce an Adapter instead of modifying existing code.