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}56// Adaptee (existing class with incompatible interface)7class AdvancedMediaPlayer {8 playVlc(fileName: string): void {9 console.log(`Playing VLC file: ${fileName}`);10 }1112 playMp4(fileName: string): void {13 console
Class Adapter (Multiple Inheritance)
1// Target2interface Target {3 request(): string;4}56// Adaptee7class Adaptee {8 specificRequest(): string {9 return "Adaptee's specific request";10 }11}1213// 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 interface2interface PaymentProcessor {3 processPayment(amount: number, currency: string): void;4}56// Legacy payment system (Adaptee)7class LegacyPaymentSystem {8 pay(amount: number): void {9 console.log(`Legacy payment: $${amount}`);10 }11}1213// 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 interface2interface Target {3 request(): void;4}56// Existing class has incompatible interface7class Adaptee {8 specificRequest(): void { /* ... */ }9}1011// Adapter makes them compatible12class Adapter implements Target {13 private adaptee: Adaptee;1415 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; // Composition34 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 method4 }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 interface2interface 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}78// Stripe adapter9class StripeAdapter implements PaymentGateway {10 private stripe: StripeClient;1112 constructorapiKey
Features:
- Unified interface: Same interface for all gateways
- Format conversion: Converts between formats
- Error mapping: Maps errors to unified format
- 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.
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
Related Topics
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.