Definition
- 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.
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.
// 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.
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.
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.
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());
}
}
// 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
BigDecimalrupees. - 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.
@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.
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.
// 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
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) │
└─────────────────────────────────┘
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.
---