← Back to principles

Design Principle

Adapter Pattern

Learn the Adapter pattern: allow incompatible interfaces to work together. Convert class interfaces into formats clients expect for seamless integration.

Adapter Pattern

Why This Matters

Think of the Adapter pattern like a travel adapter. Your device has a US plug, but the outlet is European. An adapter converts the plug so it fits. The Adapter pattern does the same for code—it converts one interface to another, allowing incompatible classes to work together.

This matters because systems often need to integrate with incompatible interfaces. You might have legacy code with one interface, new code with another interface, or third-party libraries with different interfaces. The Adapter pattern lets you make them work together without changing either side.

In interviews, when someone asks "How would you integrate legacy code with new code?", they're testing whether you understand the Adapter pattern. Do you know how to make incompatible interfaces work together? Can you integrate third-party libraries? Most engineers can't. They try to change interfaces and break existing code.

What Engineers Usually Get Wrong

Engineers often think "Adapter pattern is just wrapping objects." But Adapter pattern is more specific—it converts one interface to another, allowing incompatible interfaces to work together. This is different from Decorator (adds behavior) or Facade (simplifies interface). Understanding the difference helps you choose the right pattern.

Engineers also don't understand when to use Adapter. Use Adapter when you have incompatible interfaces that need to work together. Don't use it when you can change interfaces—changing interfaces is often simpler. Use Adapter when you can't change interfaces (legacy code, third-party libraries).

How This Breaks Systems in the Real World

A service was integrating with a third-party payment library. The library had a different interface than what the service expected. The team tried to change the service to match the library, but this broke existing code. The fix? Use Adapter pattern. Create an adapter that converts the library's interface to what the service expects. Now the service can use the library without changing existing code.

Another story: A service was migrating from an old API to a new API. The new API had a different interface. Instead of updating all code at once, they created adapters that made the new API look like the old API. This allowed gradual migration. Code using the old API continued to work, while new code used the new API directly.


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

1// Target interface (what client expects)
2interface MediaPlayer {
3 play(audioType: string, fileName: string): void;
4}
5
6// Adaptee (existing class with incompatible interface)
7class AdvancedMediaPlayer {
8 playVlc(fileName: string): void {
9 console.log(`Playing VLC file: ${fileName}`);
10 }
11
12 playMp4(fileName: string): void {
13 console

Class Adapter (Multiple Inheritance)

1// Target
2interface Target {
3 request(): string;
4}
5
6// Adaptee
7class Adaptee {
8 specificRequest(): string {
9 return "Adaptee's specific request";
10 }
11}
12
13// Adapter (extends Adaptee, implements Target)
14class Adapter extends Adaptee implements Target {
15 request(): string {
16 return this.specificRequest();
17 }
18}

Payment Gateway Adapter

1// Target interface
2interface PaymentProcessor {
3 processPayment(amount: number, currency: string): void;
4}
5
6// Legacy payment system (Adaptee)
7class LegacyPaymentSystem {
8 pay(amount: number): void {
9 console.log(`Legacy payment: $${amount}`);
10 }
11}
12
13// New payment system (Adaptee)
14class ModernPaymentSystem {
15 charge(amount: currency

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:

1// Client expects this interface
2interface Target {
3 request(): void;
4}
5
6// Existing class has incompatible interface
7class Adaptee {
8 specificRequest(): void { /* ... */ }
9}
10
11// Adapter makes them compatible
12class Adapter implements Target {
13 private adaptee: Adaptee;
14
15 request(): void {
16 this.adaptee.specificRequest();
17 }
18}

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:

1class Adapter implements Target {
2 private adaptee: Adaptee; // Composition
3
4 request(): void {
5 this.adaptee.specificRequest();
6 }
7}

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:

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

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:

1// Unified payment interface
2interface PaymentGateway {
3 processPayment(amount: number, currency: string, card: CardInfo): Promise<PaymentResult>;
4 refund(transactionId: string, amount: number): Promise<RefundResult>;
5 getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
6}
7
8// Stripe adapter
9class StripeAdapter implements PaymentGateway {
10 private stripe: StripeClient;
11
12 constructorapiKey

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

  • 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

  • Decorator Pattern - Both patterns wrap objects. Adapter converts interface, Decorator adds behavior. Understanding both helps choose the right pattern.

  • Proxy Pattern - Both patterns wrap objects. Adapter converts interface, Proxy controls access. Understanding both helps choose the right pattern.

  • SOLID Principles - Adapter pattern follows Open/Closed Principle (extend without modify). Understanding SOLID helps understand when to use adapters.

  • Separation of Concerns - Adapters separate interface conversion from business logic. Understanding separation of concerns helps understand adapter benefits.

  • Factory Pattern - Factories can create adapters. Understanding factories helps understand adapter creation patterns.

Keep exploring

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