Back to Design Patterns
Structural Pattern

Adapter Pattern

Convert the interface of a class into another interface that clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

Beginner Friendly10 min read

What is the Adapter Pattern?

The Adapter pattern is a structural design pattern that acts as a bridge between two incompatible interfaces. It wraps an existing class with a new interface so that it becomes compatible with the client's expectations — without changing either the client or the existing class.

Think of a travel power adapter: your Indian charger (two round pins) doesn't fit a UK socket (three rectangular pins). The adapter plugs into the UK socket on one side and accepts your Indian charger on the other — neither the socket nor the charger changes, but they can now work together.

In software, this is exactly what happens when you integrate a third-party library, a legacy system, or any class whose interface doesn't match what your code expects. Instead of rewriting either side, you write a thin adapter in between.

Key Idea: Make incompatible interfaces compatible by introducing a translator (adapter) between them — without modifying either the client or the existing class.

Core Participants

Target

Target Interface

The interface that the client already uses and expects. All classes that the client works with must conform to this interface. The adapter must implement this interface so the client can use it transparently.

Adaptee

Adaptee (Existing / Third-party class)

The existing class or library that has the functionality we want, but with an incompatible interface. We cannot or do not want to modify this class — it could be a third-party SDK, a legacy system, or a class from another team.

Adapter

Adapter

The translation layer. It implements the Target interface and holds a reference to an Adaptee object. It translates every Target interface call into the corresponding Adaptee call, handling any data conversion or method renaming needed.

Client

Client

The existing code that uses the Target interface. The client is completely unaware of the adapter or the adaptee — it simply works with the Target interface as it always has.

!Problem It Solves

Incompatible Interfaces Problem

Your application is built around a specific interface. A new library, service, or legacy module does exactly what you need — but its API looks completely different. Without Adapter, your only options are:

  • → Rewrite the third-party library (not possible if it's external)
  • → Rewrite all your existing client code (expensive and risky)
  • → Scatter conversion code throughout the codebase (messy and brittle)
  • → Abandon the new library entirely (missing out on its features)

Example Without Adapter

// Problem: RazorpaySDK has a different interface
// Our app calls: processor.processPayment(orderId, amount, currency)
// Razorpay needs: sdk.initiateCharge(amount, ref, currency)

// Without Adapter: conversion code leaks everywhere
class CheckoutService {
    void checkout(String orderId, double amount) {
        // Forced to know Razorpay's API details here — wrong!
        razorpaySDK.initiateCharge(amount, orderId, "INR");
        // Every caller needs to know this translation. Brittle!
    }
}

Solution With Adapter

// Adapter centralises the translation in one place
class RazorpayAdapter implements PaymentProcessor {
    void processPayment(String orderId, double amount, String currency) {
        razorpaySDK.initiateCharge(amount, orderId, currency); // translated
    }
}

// Client code stays clean and unchanged
processor.processPayment(orderId, amount, "INR"); // works with any gateway

How It Works — Step by Step

1

Identify the Target Interface

Determine the interface your client already uses. The adapter must implement this so the client needs zero changes.

2

Identify the Adaptee

The existing class or library you want to use. Understand its API — method names, parameters, return types.

3

Create the Adapter Class

Implement the Target interface. Hold a reference to the adaptee. Translate each Target method call into the appropriate adaptee call.

4

Inject and Use

Pass the adapter wherever the client expects the Target interface. The client calls the Target API; the adapter translates to the adaptee behind the scenes.

Translation Flow

Client
target.method()
Adapter
translates
adaptee.differentMethod()
Adaptee
unchanged

Object Adapter vs Class Adapter

There are two ways to implement the Adapter pattern — the approach you choose depends on your language and situation:

Object Adapter

Uses composition — the adapter holds an instance of the adaptee as a field and delegates calls to it. This is the more common and flexible approach.

class MyAdapter implements Target {
    private Adaptee adaptee; // composition
    MyAdapter(Adaptee a) { this.adaptee = a; }
    void request() { adaptee.specificRequest(); }
}

✓ Works in any language

✓ Can adapt multiple adaptees

✓ Can override adaptee behaviour

Class Adapter

Uses multiple inheritance — the adapter extends both the target and the adaptee simultaneously. Only possible in languages that support multiple inheritance (C++, Python via mixins).

// Python example (multiple inheritance)
class MyAdapter(Target, Adaptee):
    def request(self):
        self.specific_request() # from Adaptee

✓ No extra object needed

✗ Not possible in Java (single inheritance)

✗ Tightly couples adapter to both classes

Recommendation: Prefer the Object Adapter. It is more flexible, works in all languages, and aligns with the principle of favouring composition over inheritance.

Implementation

// Adapter Pattern — Payment Gateway Integration

// ─── Existing client interface ───────────────────────────────
// This is what our application already uses everywhere
interface PaymentProcessor {
    void processPayment(String orderId, double amount, String currency);
    boolean refundPayment(String transactionId);
}

// ─── Our existing implementation ─────────────────────────────
class StripeProcessor implements PaymentProcessor {
    @Override
    public void processPayment(String orderId, double amount, String currency) {
        System.out.println("[Stripe] Processing ₹" + amount + " for order " + orderId);
    }

    @Override
    public boolean refundPayment(String transactionId) {
        System.out.println("[Stripe] Refunding transaction: " + transactionId);
        return true;
    }
}

// ─── Incompatible third-party library ─────────────────────────
// Razorpay has a completely different interface — we cannot change it
class RazorpaySDK {
    public void initiateCharge(double rupees, String ref, String curr) {
        System.out.println("[Razorpay SDK] Charging ₹" + rupees
            + " | Ref: " + ref + " | Currency: " + curr);
    }

    public String cancelCharge(String paymentId) {
        System.out.println("[Razorpay SDK] Cancelling payment: " + paymentId);
        return "CANCELLED";
    }
}

// ─── Adapter ──────────────────────────────────────────────────
// Wraps RazorpaySDK and makes it look like a PaymentProcessor
class RazorpayAdapter implements PaymentProcessor {
    private RazorpaySDK razorpay;  // The adaptee

    public RazorpayAdapter(RazorpaySDK razorpay) {
        this.razorpay = razorpay;
    }

    @Override
    public void processPayment(String orderId, double amount, String currency) {
        // Translate our interface call → Razorpay SDK call
        razorpay.initiateCharge(amount, orderId, currency);
    }

    @Override
    public boolean refundPayment(String transactionId) {
        // Translate + convert return type
        String result = razorpay.cancelCharge(transactionId);
        return result.equals("CANCELLED");
    }
}

// ─── Client code ──────────────────────────────────────────────
// Client only knows PaymentProcessor — doesn't care which gateway
public class CheckoutService {
    private PaymentProcessor processor;

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

    public void checkout(String orderId, double amount) {
        processor.processPayment(orderId, amount, "INR");
    }

    public static void main(String[] args) {
        // Use Stripe directly
        CheckoutService stripeCheckout = new CheckoutService(new StripeProcessor());
        stripeCheckout.checkout("ORD-001", 1500.00);

        // Plug in Razorpay via adapter — zero changes to CheckoutService!
        RazorpaySDK razorpaySDK = new RazorpaySDK();
        CheckoutService razorpayCheckout = new CheckoutService(
            new RazorpayAdapter(razorpaySDK)
        );
        razorpayCheckout.checkout("ORD-002", 2200.00);
        // Output:
        // [Stripe]  Processing ₹1500.0 for order ORD-001
        // [Razorpay SDK] Charging ₹2200.0 | Ref: ORD-002 | Currency: INR
    }
}

When to Use Adapter

Third-Party Library Integration

When you want to use an external SDK or library but its interface doesn't match what your application already expects.

RazorpayAdapter, StripeAdapter, TwilioAdapter

Legacy System Integration

When connecting old systems with new code — you can't rewrite the legacy system, so an adapter bridges the gap.

LegacyDBAdapter, OldAPIAdapter, SOAPToRESTAdapter

Multiple Implementations, One Interface

When you want to support multiple providers (payment gateways, cloud storage, SMS services) behind a single unified interface.

S3Adapter, GCSAdapter, AzureBlobAdapter

Data Format Conversion

When two systems exchange data in different formats — XML vs JSON, CSV vs database rows, metric vs imperial units.

XMLToJSONAdapter, CSVToDBAdapter, UnitConverter

Testing with Mocks

When adapting a real service to a mock interface during testing — your tests use the same interface, the adapter decides which backend to use.

MockPaymentAdapter, FakeEmailAdapter, StubSMSAdapter

Pros & Cons

Advantages

  • Single Responsibility — translation logic lives in one place
  • Open/Closed — add new adaptees without touching client code
  • Client and adaptee are fully decoupled from each other
  • Swap out implementations at runtime by injecting a different adapter
  • Enables reuse of existing code that would otherwise be incompatible

Disadvantages

  • Adds an extra layer of indirection — slight complexity overhead
  • More classes in the codebase — one adapter per incompatible class
  • If adaptee changes its API, the adapter must be updated too
  • Sometimes overused — if you own both sides, just align the interfaces directly

Adapter vs Similar Patterns

Adapter is often confused with a few similar structural patterns. Here's how they differ:

PatternIntentInterface
AdapterMake incompatible interfaces work togetherConverts one interface to another
DecoratorAdd new behaviour to an objectKeeps the same interface
ProxyControl access to an objectKeeps the same interface
FacadeSimplify a complex subsystemDefines a new, simpler interface
BridgeSeparate abstraction from implementationDesigned upfront to be extensible

Quick rule: If you're trying to make two things work together after the fact — it's Adapter. If you're adding behaviour to something — it's Decorator. If you're simplifying something — it's Facade.

Real-World Examples

Java's Arrays.asList() and Collections

Arrays.asList() is a classic adapter — it wraps a plain array and adapts it to the List interface, letting you use array data anywhere a List is expected without copying data.

Spring's HandlerAdapter (Java)

Spring MVC uses HandlerAdapter to adapt different types of handler objects (annotated controllers, HttpRequestHandlers, Servlets) to a uniform interface that the DispatcherServlet can call without knowing which type it's dealing with.

Python's io Module

io.TextIOWrapper adapts a raw binary stream (io.RawIOBase) to a text stream interface — it translates between bytes and strings so code expecting text can work with binary sources.

ORM Database Drivers

ORMs like Sequelize, Hibernate, and SQLAlchemy use adapters (database drivers/dialects) to translate a common query interface into database-specific SQL — your code uses the same ORM API whether the backend is PostgreSQL, MySQL, or SQLite.

Redux Middleware / Store Enhancers

Redux's applyMiddleware() is an adapter that wraps the store's dispatch function, making middleware like redux-thunk and redux-saga compatible with the standard Redux dispatch interface.

What's Next?

Now that you understand Adapter, explore related Structural patterns: