Back to Structural: Adapter, Decorator, Proxy, Facade
Q5Question 5 of 6

Design using the Adapter pattern

Key Concepts
  • The Adapter pattern translates a foreign interface into the one your code expects, so incompatible classes collaborate without coupling.
  • The client depends on its own clean target interface; the adapter implements it and delegates to the third-party class.
  • Swapping one payment provider for another means writing a new adapter, not editing business logic.
  • The adapter is the right place to normalise error codes, currency units, idempotency keys, and retry semantics.
  • Dependency Inversion is satisfied because the client depends on its own abstraction, not on the vendor.
  • Open/Closed is satisfied because a new provider is a new adapter with zero changes to existing callers.

Definition

The Adapter pattern translates a foreign interface into the one your code expects, so incompatible classes collaborate without coupling.
  • Adapter – converts the interface of a class into another interface the client expects, allowing incompatible classes to work together.
  • Target interface – the clean interface defined by the client system, for example PaymentProcessor.
  • Adaptee – the third-party or legacy class with an incompatible API, for example RazorpayGateway.
  • Adapter class – implements the target interface and holds an instance of the adaptee, translating every call.
  • Isolation – the client never references the vendor directly, so swapping providers means writing a new adapter.

Think of the Adapter as a plug converter at National Bank of India when integrating an outside payment gateway. The bank's code speaks one standard "pay" language, but the Razorpay gateway speaks its own. The adapter is the travel plug that sits between them, so when Rajesh pays, the bank calls its own standard method and the adapter quietly translates it into whatever Razorpay actually expects.

Understanding the Concepts

What is the Adapter pattern for payment integration?

The client defines its own PaymentProcessor interface. The third-party gateway has a different, incompatible API. The adapter implements the target interface and holds an instance of the gateway, translating each call.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public interface PaymentProcessor {
    PaymentResult pay(String accountId, BigDecimal amount);
}

Why do you need an Adapter for gateway integration?

Without an adapter, your business logic is directly coupled to the vendor's API. If the vendor changes their API or you switch providers, every call site needs modification. The adapter isolates the vendor-specific details.

JAVA
// Without adapter: coupled to vendor (bad)
RazorpayGateway gateway = new RazorpayGateway();
RazorpayResponse resp = gateway.charge(
    new RazorpayRequest("SAV2001", 500000)); // amount in paise
// Vendor-specific types leak into your business code

// With adapter: clean interface (good)
PaymentProcessor processor = new RazorpayAdapter(new RazorpayGateway());
PaymentResult result = processor.pay("SAV2001", new BigDecimal("5000.00"));
// Business code sees only its own types

How do you implement the third-party gateway?

The third-party gateway is the adaptee. It has its own method names, parameter types, and response format that do not match your system.

JAVA
package com.bank.vendor;

public class RazorpayGateway {
    public RazorpayResponse charge(RazorpayRequest request) {
        System.out.println("Razorpay: charging "
            + request.getAmountInPaise() + " paise");
        return new RazorpayResponse("rzp_txn_001", "SUCCESS");
    }
}

public class RazorpayRequest {
    private final String merchantRef;
    private final long amountInPaise; // paise, not rupees

    public RazorpayRequest(String merchantRef, long amountInPaise) {
        this.merchantRef = merchantRef;
        this.amountInPaise = amountInPaise;
    }
    public String getMerchantRef() { return merchantRef; }
    public long getAmountInPaise() { return amountInPaise; }
}

public class RazorpayResponse {
    private final String txnId;
    private final String status;
    public RazorpayResponse(String txnId, String status) {
        this.txnId = txnId; this.status = status;
    }
    public String getTxnId() { return txnId; }
    public String getStatus() { return status; }
}

How do you implement the Adapter?

The adapter implements the target PaymentProcessor interface and translates calls to the vendor's API. It converts rupees to paise, maps arguments, and adapts the response.

JAVA
package com.bank.adapter;

import com.bank.service.PaymentProcessor;
import com.bank.service.PaymentResult;
import com.bank.vendor.RazorpayGateway;
import com.bank.vendor.RazorpayRequest;
import com.bank.vendor.RazorpayResponse;
import java.math.BigDecimal;

public class RazorpayAdapter implements PaymentProcessor {
    private final RazorpayGateway gateway;

    public RazorpayAdapter(RazorpayGateway gateway) {
        this.gateway = gateway;
    }

    @Override
    public PaymentResult pay(String accountId, BigDecimal amount) {
        // Convert rupees to paise
        long paise = amount.multiply(BigDecimal.valueOf(100)).longValue();
        RazorpayRequest request = new RazorpayRequest(accountId, paise);
        RazorpayResponse response = gateway.charge(request);
        return new PaymentResult(
            response.getTxnId(),
            "SUCCESS".equals(response.getStatus())
        );
    }
}

How do you swap to a different provider?

Writing a new adapter for a different provider requires zero changes to the business logic. This satisfies Open/Closed.

JAVA
package com.bank.adapter;

import com.bank.service.PaymentProcessor;
import com.bank.service.PaymentResult;
import com.bank.vendor.StripeGateway;
import java.math.BigDecimal;

public class StripeAdapter implements PaymentProcessor {
    private final StripeGateway gateway;

    public StripeAdapter(StripeGateway gateway) {
        this.gateway = gateway;
    }

    @Override
    public PaymentResult pay(String accountId, BigDecimal amount) {
        // Stripe uses cents, different method name
        long cents = amount.multiply(BigDecimal.valueOf(100)).longValue();
        var response = gateway.createCharge(accountId, cents);
        return new PaymentResult(response.id(), response.paid());
    }
}
JAVA
// Switch provider by changing one line of configuration
PaymentProcessor processor = new StripeAdapter(new StripeGateway());
processor.pay("SAV2001", new BigDecimal("5000.00"));
// Business code is identical regardless of provider

What should the Adapter normalise?

The adapter is the right place to tame the messy realities of third-party APIs:

  • Currency units: Razorpay uses paise, Stripe uses cents, your system uses BigDecimal rupees.
  • Error codes: each vendor has its own error format; the adapter maps them to your standard PaymentResult.
  • Idempotency keys: the adapter can generate or forward idempotency tokens.
  • Retry semantics: the adapter can wrap calls with retry logic specific to the vendor's rate limits.
JAVA
@Override
public PaymentResult pay(String accountId, BigDecimal amount) {
    try {
        long paise = amount.multiply(BigDecimal.valueOf(100)).longValue();
        RazorpayResponse resp = gateway.charge(
            new RazorpayRequest(accountId, paise));
        return new PaymentResult(resp.getTxnId(), true);
    } catch (RazorpayException e) {
        // Normalise vendor-specific error to standard result
        return new PaymentResult(null, false);
    }
}

How do you wire Adapters with Spring?

Use a configuration class that creates the right adapter based on a property. The rest of the application depends on PaymentProcessor and never knows which vendor is behind it.

JAVA
package com.bank.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;

@Configuration
public class PaymentConfig {

    @Value("${payment.provider}")
    private String provider;

    @Bean
    public PaymentProcessor paymentProcessor() {
        return switch (provider) {
            case "razorpay" -> new RazorpayAdapter(new RazorpayGateway());
            case "stripe"   -> new StripeAdapter(new StripeGateway());
            default -> throw new IllegalArgumentException(
                "Unknown provider: " + provider);
        };
    }
}

What are common mistakes?

The first mistake is letting vendor types leak into the business layer. The whole point of the adapter is isolation.

JAVA
// Wrong: vendor type in service signature
public void process(RazorpayResponse response) { } // coupled

// Correct: only your own types
public void process(PaymentResult result) { } // isolated

The second mistake is putting business logic inside the adapter. The adapter should only translate; business rules belong in the service.

What should a senior engineer know?

The adapter is the perfect place to tame vendor quirks so that everything behind it sees one consistent, well-behaved interface. At National Bank of India, wrapping a payment gateway like Razorpay behind an adapter is exactly what lets the team onboard or swap a provider by writing one class, while the payment flow that has already been tested and audited stays completely untouched.

Visual Overview

DIAGRAM
  Business Code (depends on PaymentProcessor)
       │
       │  processor.pay(accountId, amount)
       ▼
  ┌─────────────────────────────────┐
  │       RazorpayAdapter            │
  │  implements PaymentProcessor     │
  │                                  │
  │  Converts: rupees → paise        │
  │  Maps: accountId → merchantRef   │
  │  Adapts: RazorpayResponse →      │
  │          PaymentResult           │
  └───────────────┬─────────────────┘
                  │
                  ▼
  ┌─────────────────────────────────┐
  │       RazorpayGateway            │
  │  (third-party, incompatible)     │
  │  charge(RazorpayRequest)         │
  └─────────────────────────────────┘
DIAGRAM
  Swapping providers:

  Before: PaymentProcessor → RazorpayAdapter → RazorpayGateway
  After:  PaymentProcessor → StripeAdapter   → StripeGateway

  Business code is IDENTICAL in both cases

Key Takeaway

The Adapter pattern converts the interface of a class into another interface the client expects, so two otherwise incompatible pieces can work together. Rajesh uses it constantly when integrating third-party or legacy systems, because the bank's own code should speak one clean, stable language and never be shaped by a vendor's API.

The structure is a target interface that the system defines, for example a PaymentProcessor with a simple pay method, and an adapter that implements that target while holding an instance of the foreign class. The adapter translates each call: it maps the arguments, converts data formats like rupees to paise, and normalises the response, so the business code calls its own method and the adapter quietly does whatever the vendor requires underneath.

The decisive benefit is isolation. Because the bank's code depends only on its own PaymentProcessor interface and never references the vendor directly, switching from Razorpay to Stripe means writing a new adapter rather than editing business logic. This satisfies both Dependency Inversion and Open/Closed. The provider change does not ripple through the codebase.

The senior insight is that the adapter is the perfect place to tame the messy realities of external APIs: different error codes, currency units, idempotency keys, and retry semantics. Everything behind the adapter sees one consistent, well-behaved interface. At National Bank of India, wrapping a payment gateway behind an adapter is what lets the team onboard or swap a provider by writing one class, while the payment flow that has already been tested and audited stays completely untouched.

Remember one sentence: the Adapter translates a foreign interface into your own, isolating vendor quirks so business code never knows which provider is behind it.

---

Share
Use ← → arrow keys to navigate