← Back to principles

Design Principle

Adapter Pattern

Learn the Adapter pattern: allow incompatible interfaces to work together. Convert interface of a class into another interface clients expect.

The Adapter Pattern allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces by wrapping an object to make it compatible with another interface.


What is the Adapter Pattern?

Adapter Pattern provides:

  • Interface conversion: Converts one interface to another
  • Compatibility: Makes incompatible interfaces work together
  • Legacy integration: Integrates legacy code with new systems
  • Reusability: Reuses existing classes with different interfaces

Use cases:

  • Integrating third-party libraries
  • Working with legacy code
  • Converting data formats
  • Making incompatible classes work together

Structure

Client
  └─ uses Target interface

Target (interface)
  └─ request()

Adapter (implements Target)
  └─ adaptee: Adaptee
  └─ request() (calls adaptee.specificRequest())

Adaptee (existing class)
  └─ specificRequest()

Examples

Object Adapter

// Target interface (what client expects)
interface MediaPlayer {
  play(audioType: string, fileName: string): void;
}

// Adaptee (existing class with incompatible interface)
class AdvancedMediaPlayer {
  playVlc(fileName: string): void {
    console.log(`Playing VLC file: ${fileName}`);
  }
  
  playMp4(fileName: string): void {
    console.log(`Playing MP4 file: ${fileName}`);
  }
}

// Adapter (adapts Adaptee to Target)
class MediaAdapter implements MediaPlayer {
  private advancedPlayer: AdvancedMediaPlayer;
  
  constructor(audioType: string) {
    this.advancedPlayer = new AdvancedMediaPlayer();
  }
  
  play(audioType: string, fileName: string): void {
    if (audioType === "vlc") {
      this.advancedPlayer.playVlc(fileName);
    } else if (audioType === "mp4") {
      this.advancedPlayer.playMp4(fileName);
    }
  }
}

// Client
class AudioPlayer implements MediaPlayer {
  play(audioType: string, fileName: string): void {
    if (audioType === "mp3") {
      console.log(`Playing MP3 file: ${fileName}`);
    } else if (audioType === "vlc" || audioType === "mp4") {
      const adapter = new MediaAdapter(audioType);
      adapter.play(audioType, fileName);
    } else {
      console.log(`Invalid media type: ${audioType}`);
    }
  }
}

// Usage
const player = new AudioPlayer();
player.play("mp3", "song.mp3");
player.play("vlc", "movie.vlc");
player.play("mp4", "video.mp4");

Class Adapter (Multiple Inheritance)

// Target
interface Target {
  request(): string;
}

// Adaptee
class Adaptee {
  specificRequest(): string {
    return "Adaptee's specific request";
  }
}

// Adapter (extends Adaptee, implements Target)
class Adapter extends Adaptee implements Target {
  request(): string {
    return this.specificRequest();
  }
}

// Usage
const adapter = new Adapter();
console.log(adapter.request());

Payment Gateway Adapter

// Target interface
interface PaymentProcessor {
  processPayment(amount: number, currency: string): void;
}

// Legacy payment system (Adaptee)
class LegacyPaymentSystem {
  pay(amount: number): void {
    console.log(`Legacy payment: $${amount}`);
  }
}

// New payment system (Adaptee)
class ModernPaymentSystem {
  charge(amount: number, currency: string): void {
    console.log(`Modern payment: ${amount} ${currency}`);
  }
}

// Adapter for legacy system
class LegacyPaymentAdapter implements PaymentProcessor {
  private legacySystem: LegacyPaymentSystem;
  
  constructor() {
    this.legacySystem = new LegacyPaymentSystem();
  }
  
  processPayment(amount: number, currency: string): void {
    // Convert currency and call legacy method
    const usdAmount = this.convertToUSD(amount, currency);
    this.legacySystem.pay(usdAmount);
  }
  
  private convertToUSD(amount: number, currency: string): number {
    // Currency conversion logic
    const rates: { [key: string]: number } = {
      "EUR": 1.1,
      "GBP": 1.3,
      "USD": 1.0
    };
    return amount * (rates[currency] || 1.0);
  }
}

// Adapter for modern system
class ModernPaymentAdapter implements PaymentProcessor {
  private modernSystem: ModernPaymentSystem;
  
  constructor() {
    this.modernSystem = new ModernPaymentSystem();
  }
  
  processPayment(amount: number, currency: string): void {
    this.modernSystem.charge(amount, currency);
  }
}

// Client
class PaymentService {
  private processor: PaymentProcessor;
  
  constructor(processor: PaymentProcessor) {
    this.processor = processor;
  }
  
  makePayment(amount: number, currency: string): void {
    this.processor.processPayment(amount, currency);
  }
}

// Usage
const legacyAdapter = new LegacyPaymentAdapter();
const paymentService1 = new PaymentService(legacyAdapter);
paymentService1.makePayment(100, "EUR");

const modernAdapter = new ModernPaymentAdapter();
const paymentService2 = new PaymentService(modernAdapter);
paymentService2.makePayment(100, "USD");

Common Pitfalls

  • Over-adapting: Creating adapters for everything. Fix: Only use when interfaces are truly incompatible
  • Performance overhead: Adapter adds layer. Fix: Consider performance impact
  • Complex adapters: Adapters become too complex. Fix: Keep adapters simple, single responsibility
  • Tight coupling: Adapter tightly coupled to adaptee. Fix: Use interfaces, dependency injection

Interview Questions

Beginner

Q: What is the Adapter pattern and when would you use it?

A:

Adapter Pattern allows objects with incompatible interfaces to work together.

How it works:

  • Wraps incompatible object: Adapter wraps adaptee
  • Implements target interface: Adapter implements interface client expects
  • Translates calls: Converts calls from target to adaptee

Example:

// Client expects this interface
interface Target {
  request(): void;
}

// Existing class has incompatible interface
class Adaptee {
  specificRequest(): void { /* ... */ }
}

// Adapter makes them compatible
class Adapter implements Target {
  private adaptee: Adaptee;
  
  request(): void {
    this.adaptee.specificRequest();
  }
}

Use cases:

  • Legacy integration: Integrate legacy code with new systems
  • Third-party libraries: Use libraries with incompatible interfaces
  • Data format conversion: Convert between different data formats
  • API integration: Integrate different APIs

Intermediate

Q: Explain the difference between Object Adapter and Class Adapter. What are the trade-offs?

A:

Object Adapter (Composition):

Uses composition to adapt:

class Adapter implements Target {
  private adaptee: Adaptee;  // Composition
  
  request(): void {
    this.adaptee.specificRequest();
  }
}

Advantages:

  • Flexible: Can adapt multiple adaptees
  • Single responsibility: Adapter only adapts
  • No inheritance: Doesn't inherit adaptee's interface

Disadvantages:

  • Extra object: Additional object in memory
  • Indirection: Extra method call

Class Adapter (Inheritance):

Uses inheritance to adapt:

class Adapter extends Adaptee implements Target {
  request(): void {
    this.specificRequest();  // Inherited method
  }
}

Advantages:

  • Direct access: Direct access to adaptee methods
  • No extra object: No additional object
  • Efficient: Slightly more efficient

Disadvantages:

  • Multiple inheritance: Requires multiple inheritance (not in all languages)
  • Tight coupling: Tightly coupled to adaptee
  • Less flexible: Can only adapt one adaptee

When to use:

  • Object Adapter: Preferred in most cases (more flexible)
  • Class Adapter: When you need direct access, language supports multiple inheritance

Senior

Q: Design an adapter system for integrating multiple payment gateways (Stripe, PayPal, Square) with a unified payment interface. Handle different response formats, error handling, and transaction status mapping.

A:

// Unified payment interface
interface PaymentGateway {
  processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult>;
  refund(transactionId: string, amount: number): Promise<RefundResult>;
  getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
}

// Stripe adapter
class StripeAdapter implements PaymentGateway {
  private stripe: StripeClient;
  
  constructor(apiKey: string) {
    this.stripe = new StripeClient(apiKey);
  }
  
  async processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult> {
    try {
      // Convert to Stripe format
      const stripeCharge = {
        amount: Math.round(amount * 100), // Convert to cents
        currency: currency.toLowerCase(),
        source: {
          number: card.number,
          exp_month: card.expMonth,
          exp_year: card.expYear,
          cvc: card.cvv
        }
      };
      
      const response = await this.stripe.charges.create(stripeCharge);
      
      // Convert Stripe response to unified format
      return {
        success: true,
        transactionId: response.id,
        amount: response.amount / 100,
        currency: response.currency.toUpperCase(),
        status: this.mapStatus(response.status)
      };
    } catch (error) {
      return {
        success: false,
        error: this.mapError(error)
      };
    }
  }
  
  async refund(transactionId: string, amount: number): Promise<RefundResult> {
    const refund = await this.stripe.refunds.create({
      charge: transactionId,
      amount: Math.round(amount * 100)
    });
    
    return {
      success: true,
      refundId: refund.id
    };
  }
  
  async getTransactionStatus(transactionId: string): Promise<TransactionStatus> {
    const charge = await this.stripe.charges.retrieve(transactionId);
    return this.mapStatus(charge.status);
  }
  
  private mapStatus(stripeStatus: string): TransactionStatus {
    const statusMap: { [key: string]: TransactionStatus } = {
      "succeeded": "completed",
      "pending": "pending",
      "failed": "failed"
    };
    return statusMap[stripeStatus] || "unknown";
  }
  
  private mapError(error: any): string {
    return error.message || "Payment processing failed";
  }
}

// PayPal adapter
class PayPalAdapter implements PaymentGateway {
  private paypal: PayPalClient;
  
  constructor(clientId: string, secret: string) {
    this.paypal = new PayPalClient(clientId, secret);
  }
  
  async processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult> {
    try {
      // Convert to PayPal format
      const payment = {
        intent: "sale",
        payer: {
          payment_method: "credit_card",
          funding_instruments: [{
            credit_card: {
              number: card.number,
              type: this.mapCardType(card.type),
              expire_month: card.expMonth,
              expire_year: card.expYear,
              cvv2: card.cvv
            }
          }]
        },
        transactions: [{
          amount: {
            total: amount.toString(),
            currency: currency
          }
        }]
      };
      
      const response = await this.paypal.payment.create(payment);
      
      // Convert PayPal response to unified format
      return {
        success: response.state === "approved",
        transactionId: response.id,
        amount: parseFloat(response.transactions[0].amount.total),
        currency: response.transactions[0].amount.currency,
        status: this.mapStatus(response.state)
      };
    } catch (error) {
      return {
        success: false,
        error: this.mapError(error)
      };
    }
  }
  
  // Similar methods for refund and status...
  
  private mapCardType(type: string): string {
    const typeMap: { [key: string]: string } = {
      "visa": "visa",
      "mastercard": "mastercard",
      "amex": "amex"
    };
    return typeMap[type] || "visa";
  }
  
  private mapStatus(paypalState: string): TransactionStatus {
    const statusMap: { [key: string]: TransactionStatus } = {
      "approved": "completed",
      "pending": "pending",
      "failed": "failed"
    };
    return statusMap[paypalState] || "unknown";
  }
  
  private mapError(error: any): string {
    return error.details?.[0]?.description || "Payment processing failed";
  }
}

// Payment service using adapters
class PaymentService {
  private gateway: PaymentGateway;
  
  constructor(gateway: PaymentGateway) {
    this.gateway = gateway;
  }
  
  async processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult> {
    return await this.gateway.processPayment(amount, currency, card);
  }
}

// Usage
const stripeAdapter = new StripeAdapter("sk_test_...");
const paymentService = new PaymentService(stripeAdapter);
const result = await paymentService.processPayment(100, "USD", cardInfo);

Features:

  1. Unified interface: Same interface for all gateways
  2. Format conversion: Converts between formats
  3. Error mapping: Maps errors to unified format
  4. Status mapping: Maps statuses to unified format

Key Takeaways

  • Adapter pattern: Makes incompatible interfaces work together
  • Object adapter: Uses composition (preferred)
  • Class adapter: Uses inheritance (when supported)
  • Use cases: Legacy integration, third-party libraries, API integration
  • Benefits: Reusability, compatibility, integration
  • Best practices: Keep adapters simple, use interfaces, handle errors properly

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.