Skip to main content

Command Palette

Search for a command to run...

Adapter Pattern: Make Incompatible Interfaces Work

Learn: Adapter Pattern: Make Incompatible Interfaces Work

Updated
β€’6 min readβ€’View as Markdown
T

Welcome to TopperBlog! πŸ‘‹

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: β€’ AI/ML Engineering & LLMs β€’ Web3 & Blockchain Development
β€’ System Design & Architecture β€’ Interview Preparation (FAANG) β€’ Freelancing & Remote Work β€’ Modern Tech Stacks (Next.js, React, Rust, TypeScript) β€’ Performance Optimization & Best Practices

πŸ’Ό Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

πŸ“š 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Adapter Pattern: Make Incompatible Interfaces Work

Overview

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces, converting the interface of a class into another interface clients expect.


Problem

Scenario

Imagine you're building a payment processing system. Your application uses a PaymentProcessor interface that expects:

process(amount: number): boolean

However, you need to integrate with a third-party payment gateway (LegacyPaymentGateway) that has a completely different interface:

executeTransaction(value: number, currency: string): {success: boolean, transactionId: string}

Challenges:

  • You cannot modify the third-party library code
  • Your application code expects the PaymentProcessor interface
  • Direct integration would require changing all client code
  • Multiple incompatible interfaces need to work together seamlessly
  • Code becomes tightly coupled and difficult to maintain

Why It's a Problem

  • Interface Mismatch: The third-party library doesn't conform to your expected interface
  • Rigidity: Changing client code to accommodate different interfaces violates the Open/Closed Principle
  • Maintainability: Multiple integration points become scattered and hard to manage
  • Reusability: Cannot easily swap implementations without refactoring

Solution

Concept

Create an Adapter class that:

  1. Implements the interface your application expects
  2. Wraps the incompatible object (adaptee)
  3. Translates method calls from the expected interface to the adaptee's interface
  4. Returns results in the format your application expects

Benefits

  • Decoupling: Client code remains independent of third-party interfaces
  • Flexibility: Easy to add new adapters for different incompatible interfaces
  • Reusability: Adapters can be reused across different parts of the application
  • Maintainability: Changes to third-party libraries only affect the adapter
  • Single Responsibility: Each adapter handles one specific incompatibility

Structure

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Client      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ uses
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Target         │◄────────│    Adapter       β”‚
β”‚  Interface      β”‚ adapts  β”‚                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
                            β”‚ β”‚   Adaptee    β”‚ β”‚
                            β”‚ β”‚ (Incompatible)
                            β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Code Implementation

1. Class Adapter Pattern (Inheritance)

// Target Interface - What the client expects
interface PaymentProcessor {
  process(amount: number): boolean;
}

// Adaptee - Third-party library with incompatible interface
class LegacyPaymentGateway {
  executeTransaction(
    value: number,
    currency: string
  ): { success: boolean; transactionId: string } {
    // Simulating legacy payment processing
    console.log(`Processing ${value} ${currency} via legacy gateway`);
    return {
      success: value > 0,
      transactionId: `TXN-${Date.now()}`,
    };
  }
}

// Adapter - Makes LegacyPaymentGateway compatible with PaymentProcessor
class PaymentGatewayAdapter
  extends LegacyPaymentGateway
  implements PaymentProcessor
{
  process(amount: number): boolean {
    const result = this.executeTransaction(amount, "USD");
    console.log(`Adapter: Converted process() to executeTransaction()`);
    return result.success;
  }
}

// Client Code
class CheckoutService {
  constructor(private paymentProcessor: PaymentProcessor) {}

  checkout(amount: number): void {
    if (this.paymentProcessor.process(amount)) {
      console.log("βœ“ Payment successful");
    } else {
      console.log("βœ— Payment failed");
    }
  }
}

// Usage
const adapter = new PaymentGatewayAdapter();
const checkout = new CheckoutService(adapter);
checkout.checkout(100); // Works seamlessly!

2. Object Adapter Pattern (Composition)

// Target Interface
interface DataSource {
  read(): string;
  write(data: string): void;
}

// Adaptee - Incompatible third-party library
class LegacyDatabase {
  getData(): string {
    return "Legacy data format";
  }

  saveData(info: string): void {
    console.log(`Saving to legacy DB: ${info}`);
  }
}

// Adapter using composition
class DatabaseAdapter implements DataSource {
  constructor(private legacyDb: LegacyDatabase) {}

  read(): string {
    const legacyData = this.legacyDb.getData();
    // Transform legacy format to modern format
    return `[MODERN] ${legacyData}`;
  }

  write(data: string): void {
    // Transform modern format to legacy format
    const legacyFormat = data.replace("[MODERN]", "");
    this.legacyDb.saveData(legacyFormat);
  }
}

// Client Code
class ApplicationService {
  constructor(private dataSource: DataSource) {}

  loadAndProcess(): void {
    const data = this.dataSource.read();
    console.log(`Processing: ${data}`);
    this.dataSource.write(`${data} - processed`);
  }
}

// Usage
const legacyDb = new LegacyDatabase();
const adapter = new DatabaseAdapter(legacyDb);
const app = new ApplicationService(adapter);
app.loadAndProcess();

3. Real-World Example: Media Player

// Target Interface
interface MediaPlayer {
  play(filename: string): void;
}

// Adaptees - Different media formats
class VLCPlayer {
  playVLC(filename: string): void {
    console.log(`VLC playing: ${filename}`);
  }
}

class QuickTimePlayer {
  playQuickTime(filename: string): void {
    console.log(`QuickTime playing: ${filename}`);
  }
}

// Adapters for each format
class VLCAdapter implements MediaPlayer {
  constructor(private vlcPlayer: VLCPlayer) {}

  play(filename: string): void {
    if (filename.endsWith(".avi")) {
      this.vlcPlayer.playVLC(filename);
    }
  }
}

class QuickTimeAdapter implements MediaPlayer {
  constructor(private qtPlayer: QuickTimePlayer) {}

  play(filename: string): void {
    if (filename.endsWith(".mov")) {
      this.qtPlayer.playQuickTime(filename);
    }
  }
}

// Client - Works with any adapter
class AudioPlayer {
  private players: Map<string, MediaPlayer> = new Map();

  registerPlayer(format: string, player: MediaPlayer): void {
    this.players.set(format, player);
  }

  playMedia(filename: string): void {
    const extension = filename.split(".").pop() || "";
    const player = this.players.get(extension);

    if (player) {
      player.play(filename);
    } else {
      console.log(`No player found for ${extension}`);
    }
  }
}

// Usage
const audioPlayer = new AudioPlayer();
audioPlayer.registerPlayer("avi", new VLCAdapter(new VLCPlayer()));
audioPlayer.registerPlayer("mov", new QuickTimeAdapter(new QuickTimePlayer()));

audioPlayer.playMedia("movie.avi");  // VLC playing: movie.avi
audioPlayer.playMedia("video.mov");  // QuickTime playing: video.mov

4. Advanced Example: Multi-Adapter System

// Target Interface
interface Logger {
  log(level: string, message: string): void;
}

// Multiple Adaptees
class ConsoleLogger {
  output(msg: string): void {
    console.log(`[CONSOLE] ${msg}`);
  }
}

class FileLogger {
  writeToFile(filename: string, content: string): void {
    console.log(`[FILE] Writing to ${filename}: ${content}`);
  }
}

class RemoteLogger {
  sendToServer(endpoint: string, payload: object): void {
    console.log(`[REMOTE] Sending to ${endpoint}:`, payload);
  }
}

// Adapters
class ConsoleLoggerAdapter implements Logger {
  constructor(private logger: ConsoleLogger) {}

  log(level: string, message: string): void {
    this.logger.output(`[${level}] ${message}`);
  }
}

class FileLoggerAdapter implements Logger {
  constructor(private logger: FileLogger) {}

  log(level: string, message: string): void {
    this.logger.writeToFile("app.log", `[${level}] ${message}`);
  }
}

class RemoteLoggerAdapter implements Logger {
  constructor(private logger: RemoteLogger) {}

  log(level: string, message: string): void {
    this.logger.sendToServer("/api/logs", {
      level,
      message,
      timestamp: new Date().toISOString(),
    });
  }
}

// Composite Logger using adapters
class CompositeLogger implements Logger {
  private loggers: Logger[] = [];

  addLogger(logger: Logger): void {
    this.loggers.push(logger);
  }

  log(level: string, message: string): void {
    this.loggers.forEach((logger) => logger.log(level, message));
  }
}

// Usage
const compositeLogger = new CompositeLogger();
compositeLogger.addLogger(new ConsoleLoggerAdapter(new ConsoleLogger()));
compositeLogger.addLogger(new FileLoggerAdapter(new FileLogger()));
compositeLogger.addLogger(new RemoteLoggerAdapter(new RemoteLogger()));

compositeLogger.log("ERROR", "Database connection failed");
// Logs to console, file, and remote server simultaneously

Key Characteristics

AspectDetails
TypeStructural Pattern
PurposeConvert incompatible interfaces
ParticipantsClient, Target, Adapter, Adaptee
ComplexityLow to Medium
Use CasesLegacy integration, third-party libraries, format conversion
VariantsClass Adapter (inheritance), Object Adapter (composition)

When to Use

βœ… Use Adapter Pattern when:

  • Integrating third-party libraries with incompatible interfaces
  • Working with legacy code that cannot be modified
  • Need to make multiple incompatible interfaces work together
  • Want to decouple client code from specific implementations
  • Creating a unified interface for different implementations

❌ Avoid when:

  • You can modify the incompatible interface directly
  • The adaptation logic is too complex (consider Facade instead)
  • Performance is critical and extra layers matter
  • Only adapting a single interface (might be over-engineering)

Comparison with Similar Patterns

PatternPurposeKey Difference
AdapterMake incompatible interfaces compatibleWorks with existing interfaces
BridgeDecouple abstraction from implementationDesigned upfront for flexibility
FacadeSimplify complex subsystemsProvides simplified interface
DecoratorAdd behavior dynamicallyEnhances existing interface

Conclusion

The Adapter Pattern is essential for integrating incompatible systems without modifying existing code. It promotes loose coupling, enhances maintainability, and provides a clean way to handle interface mismatches. By using adapters, you create a flexible architecture that can easily accommodate new integrations and changes to third-party dependencies.