Adapter Pattern

Introduction

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. Think of it like a power adapter you use when traveling abroad — your laptop plug does not change, the foreign outlet does not change, but the adapter bridges the gap between them.

In software, you will frequently encounter situations where you need to integrate a third-party library, legacy system, or external service whose interface does not match what your application expects. Rewriting either side is risky and expensive. The Adapter Pattern solves this elegantly.

The Problem

Imagine your e-commerce application processes payments through a clean, modern interface. A new business requirement demands integration with a legacy payment gateway that has a completely different API — different method names, different parameter formats, and different response structures.

You cannot modify the legacy system (it is maintained by another team or vendor), and you should not pollute your existing codebase with legacy-specific logic scattered across multiple files.

The Solution

Create an adapter class that implements your application’s expected interface while internally translating calls to the legacy system’s API. Your application code continues to work with the interface it already knows, completely unaware that a different system is handling the actual payment processing behind the scenes.

The adapter acts as a translator, converting requests from one format to another without either side needing to change.

Key Principle

The Adapter Pattern follows the Open/Closed Principle — your existing code remains open for extension (new payment gateways) but closed for modification (no changes to existing business logic). It also promotes programming to interfaces, keeping your code decoupled from specific implementations.

Java Example

Here we adapt a legacy payment gateway to work with our modern payment interface.

// Modern payment interface our application expects
public interface PaymentProcessor {
    PaymentResult processPayment(String customerId, double amount, String currency);
    PaymentResult refund(String transactionId, double amount);
}

// Result object
public class PaymentResult {
    private boolean success;
    private String transactionId;
    private String message;

    public PaymentResult(boolean success, String transactionId, String message) {
        this.success = success;
        this.transactionId = transactionId;
        this.message = message;
    }

    // Getters
    public boolean isSuccess() { return success; }
    public String getTransactionId() { return transactionId; }
    public String getMessage() { return message; }
}

// Legacy payment gateway with a completely different interface
public class LegacyPaymentGateway {

    public Map<String, Object> submitTransaction(Map<String, String> params) {
        System.out.println("Legacy gateway processing: " + params);
        Map<String, Object> response = new HashMap<>();
        response.put("status_code", 0);  // 0 = success in legacy system
        response.put("ref_number", "LEG-" + System.currentTimeMillis());
        response.put("status_msg", "Transaction approved");
        return response;
    }

    public Map<String, Object> reverseTransaction(String refNumber, String amountInCents) {
        System.out.println("Legacy gateway reversing: " + refNumber);
        Map<String, Object> response = new HashMap<>();
        response.put("status_code", 0);
        response.put("ref_number", "REV-" + System.currentTimeMillis());
        response.put("status_msg", "Reversal approved");
        return response;
    }
}

// Adapter: bridges modern interface to legacy gateway
public class LegacyPaymentAdapter implements PaymentProcessor {

    private final LegacyPaymentGateway legacyGateway;

    public LegacyPaymentAdapter(LegacyPaymentGateway legacyGateway) {
        this.legacyGateway = legacyGateway;
    }

    @Override
    public PaymentResult processPayment(String customerId, double amount, String currency) {
        // Translate modern parameters to legacy format
        Map<String, String> params = new HashMap<>();
        params.put("cust_id", customerId);
        params.put("amt", String.valueOf((int)(amount * 100))); // Legacy uses cents
        params.put("curr", currency.toUpperCase());

        Map<String, Object> response = legacyGateway.submitTransaction(params);

        // Translate legacy response to modern format
        boolean success = (int) response.get("status_code") == 0;
        return new PaymentResult(
            success,
            (String) response.get("ref_number"),
            (String) response.get("status_msg")
        );
    }

    @Override
    public PaymentResult refund(String transactionId, double amount) {
        String amountInCents = String.valueOf((int)(amount * 100));
        Map<String, Object> response = legacyGateway.reverseTransaction(transactionId, amountInCents);

        boolean success = (int) response.get("status_code") == 0;
        return new PaymentResult(
            success,
            (String) response.get("ref_number"),
            (String) response.get("status_msg")
        );
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Wrap legacy gateway in adapter
        LegacyPaymentGateway legacyGateway = new LegacyPaymentGateway();
        PaymentProcessor processor = new LegacyPaymentAdapter(legacyGateway);

        // Application code works with the modern interface
        PaymentResult result = processor.processPayment("CUST-123", 49.99, "USD");
        System.out.println("Success: " + result.isSuccess());
        System.out.println("Transaction ID: " + result.getTransactionId());
    }
}

Python Example

The same payment gateway adapter scenario in Python.

from abc import ABC, abstractmethod
from dataclasses import dataclass
import time


@dataclass
class PaymentResult:
    success: bool
    transaction_id: str
    message: str


# Modern payment interface our application expects
class PaymentProcessor(ABC):

    @abstractmethod
    def process_payment(self, customer_id: str, amount: float, currency: str) -> PaymentResult:
        pass

    @abstractmethod
    def refund(self, transaction_id: str, amount: float) -> PaymentResult:
        pass


# Legacy payment gateway with a completely different interface
class LegacyPaymentGateway:

    def submit_transaction(self, params: dict) -> dict:
        print(f"Legacy gateway processing: {params}")
        return {
            "status_code": 0,  # 0 = success in legacy system
            "ref_number": f"LEG-{int(time.time() * 1000)}",
            "status_msg": "Transaction approved"
        }

    def reverse_transaction(self, ref_number: str, amount_in_cents: str) -> dict:
        print(f"Legacy gateway reversing: {ref_number}")
        return {
            "status_code": 0,
            "ref_number": f"REV-{int(time.time() * 1000)}",
            "status_msg": "Reversal approved"
        }


# Adapter: bridges modern interface to legacy gateway
class LegacyPaymentAdapter(PaymentProcessor):

    def __init__(self, legacy_gateway: LegacyPaymentGateway):
        self._legacy_gateway = legacy_gateway

    def process_payment(self, customer_id: str, amount: float, currency: str) -> PaymentResult:
        # Translate modern parameters to legacy format
        params = {
            "cust_id": customer_id,
            "amt": str(int(amount * 100)),  # Legacy uses cents
            "curr": currency.upper()
        }

        response = self._legacy_gateway.submit_transaction(params)

        return PaymentResult(
            success=response["status_code"] == 0,
            transaction_id=response["ref_number"],
            message=response["status_msg"]
        )

    def refund(self, transaction_id: str, amount: float) -> PaymentResult:
        amount_in_cents = str(int(amount * 100))
        response = self._legacy_gateway.reverse_transaction(transaction_id, amount_in_cents)

        return PaymentResult(
            success=response["status_code"] == 0,
            transaction_id=response["ref_number"],
            message=response["status_msg"]
        )


# Usage
if __name__ == "__main__":
    legacy_gateway = LegacyPaymentGateway()
    processor = LegacyPaymentAdapter(legacy_gateway)

    result = processor.process_payment("CUST-123", 49.99, "USD")
    print(f"Success: {result.success}")
    print(f"Transaction ID: {result.transaction_id}")

When to Use

  • Third-party integration — When integrating an external library or service whose interface does not match your application’s expectations
  • Legacy system migration — When gradually migrating from an old system and need both old and new code to coexist
  • Multiple vendor support — When your application must support multiple vendors (payment gateways, cloud providers, email services) through a unified interface
  • Testing — When you need to wrap a hard-to-test dependency behind a testable interface

Real-World Usage

  • Spring FrameworkHandlerAdapter adapts different handler types to the DispatcherServlet
  • Java I/OInputStreamReader adapts a byte stream (InputStream) to a character stream (Reader)
  • SLF4J — Acts as an adapter facade over different logging frameworks (Log4j, Logback, java.util.logging)
  • Python’s csv.DictReader — Adapts CSV file reading into dictionary-based access
  • Django REST Framework — Serializers adapt between Django models and JSON representations



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *