Skip to contentJMRG
All posts

Adapter Pattern: Integrating Incompatible Interfaces

design-patternsstructuraladapter

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 --> Adaptee

Think 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).