Adapter Pattern: Integrating Incompatible Interfaces

What is the Adapter Pattern?
The Adapter pattern allows incompatible interfaces to collaborate. It converts the interface of a class into another interface that the client expects, acting as a translator between systems with different APIs.
classDiagram
class Target {
<<interface>>
+request()
}
class Adapter {
-adaptee: Adaptee
+request()
}
class Adaptee {
+specificRequest()
}
class Client
Client --> Target
Target <|.. Adapter
Adapter --> AdapteeThink of a power plug adapter: it converts a European plug to an American one without changing either device.
The Problem It Solves
Your application uses a standard interface, but each external SDK has its own API:
interface PaymentProcessor {
charge(amount: number, currency: string, token: string): Promise<PaymentResult>;
refund(transactionId: string, amount?: number): Promise<RefundResult>;
}
class StripeSDK {
createPaymentIntent(params: { amount: number; currency: string }): Promise<StripeIntent> { }
}
class PayPalAPI {
createOrder(params: { intent: string; purchase_units: any[] }): Promise<PayPalOrder> { }
}Without Adapter: You'd have to modify all code for each different API.
The Solution: Adapter
1. Define the Target Interface
interface PaymentResult {
success: boolean;
transactionId: string;
amount: number;
currency: string;
timestamp: Date;
}
/** Unified interface for payment processors */
interface PaymentProcessor {
charge(amount: number, currency: string, method: string): Promise<PaymentResult>;
refund(transactionId: string, amount?: number): Promise<RefundResult>;
}2. Create the Stripe Adapter
/** Adapts Stripe SDK to our PaymentProcessor interface */
class StripeAdapter implements PaymentProcessor {
constructor(private stripe: StripeSDK) {}
async charge(amount: number, currency: string, method: string): Promise<PaymentResult> {
const stripeAmount = Math.round(amount * 100);
const intent = await this.stripe.createPaymentIntent({
amount: stripeAmount,
currency: currency.toLowerCase(),
payment_method: method,
confirm: true,
});
return {
success: intent.status === 'succeeded',
transactionId: intent.id,
amount: intent.amount / 100,
currency: intent.currency.toUpperCase(),
timestamp: new Date(intent.created * 1000),
};
}
async refund(transactionId: string, amount?: number): Promise<RefundResult> {
const refund = await this.stripe.createRefund({
payment_intent: transactionId,
amount: amount ? Math.round(amount * 100) : undefined,
});
return {
success: refund.status === 'succeeded',
refundId: refund.id,
amount: refund.amount / 100,
};
}
}3. Create the PayPal Adapter
/** Adapts PayPal API to our PaymentProcessor interface */
class PayPalAdapter implements PaymentProcessor {
constructor(private paypal: PayPalAPI) {}
async charge(amount: number, currency: string, method: string): Promise<PaymentResult> {
const order = await this.paypal.createOrder({
intent: 'CAPTURE',
purchase_units: [{
amount: { value: amount.toFixed(2), currency_code: currency.toUpperCase() },
}],
});
const capture = await this.paypal.captureOrder(order.id);
return {
success: capture.status === 'COMPLETED',
transactionId: capture.id,
amount: parseFloat(capture.purchase_units[0].payments.captures[0].amount.value),
currency: capture.purchase_units[0].payments.captures[0].amount.currency_code,
timestamp: new Date(),
};
}
async refund(transactionId: string, amount?: number): Promise<RefundResult> {
const refund = await this.paypal.refundCapture(transactionId, {
amount: amount ? { value: amount.toFixed(2) } : undefined,
});
return {
success: refund.status === 'COMPLETED',
refundId: refund.id,
amount: parseFloat(refund.amount.value),
};
}
}4. Use the Adapters
/** Business service that uses any processor */
class PaymentService {
constructor(private processor: PaymentProcessor) {}
async processPayment(amount: number, currency: string, method: string): Promise<PaymentResult> {
return this.processor.charge(amount, currency, method);
}
}
const stripeService = new PaymentService(new StripeAdapter(new StripeSDK('sk_xxx')));
const paypalService = new PaymentService(new PayPalAdapter(new PayPalAPI('id', 'secret')));
const result1 = await stripeService.processPayment(99.99, 'USD', 'pm_xxx');
const result2 = await paypalService.processPayment(99.99, 'USD', 'token_xxx');Benefits of the Adapter Pattern
| Aspect | Benefit |
|---|---|
| Decoupling | Client code doesn't depend on external SDKs |
| Single Responsibility | Each adapter handles one translation |
| Open/Closed | Add providers without modifying code |
| Testability | Easy to mock in tests |
When to Use Adapter
- Integrate third-party libraries with incompatible interfaces
- Migrate from a legacy API to a new one
- Create wrappers for external services
- Normalize multiple data sources
When NOT to Use Adapter
- If the external interface is already compatible
- If there's only one implementation that will never change
Conclusion
The Adapter pattern is essential for cleanly integrating external systems. It acts as a translator that allows your internal code to remain stable while external services change or new ones are added.
This is the last article in our introductory series. I invite you to explore more design patterns and apply them in your projects.
Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).