Skip to content

Adapter Pattern

Make incompatible interfaces work together - bridge the gap between old and new!

Adapter Pattern: Making Incompatible Interfaces Compatible

Section titled “Adapter Pattern: Making Incompatible Interfaces Compatible”

Now let’s dive into the Adapter Pattern - one of the most commonly used structural design patterns.

Imagine you’re traveling to Europe with a US phone charger. The plug doesn’t fit European sockets! You need an adapter to make your US plug work with European sockets. The Adapter Pattern works the same way!

The Adapter Pattern lets incompatible interfaces work together. It acts as a bridge between two incompatible interfaces, allowing them to collaborate without modifying their source code.

The Adapter Pattern is useful when:

  1. You need to use existing classes - But their interfaces don’t match what you need
  2. Integrating third-party libraries - Library interface doesn’t match your code
  3. Legacy code integration - Old code needs to work with new code
  4. Multiple incompatible interfaces - Need to make them work together
  5. You want to avoid modifying - Don’t want to change existing code

What Happens If We Don’t Use Adapter Pattern?

Section titled “What Happens If We Don’t Use Adapter Pattern?”

Without the Adapter Pattern, you might:

  • Modify existing code - Risk breaking working code
  • Create wrapper code - Scattered throughout your codebase
  • Tight coupling - Code depends on incompatible interfaces
  • Code duplication - Similar conversion logic repeated everywhere
  • Violate Open/Closed Principle - Need to modify code to integrate new systems

Let’s start with a super simple example that anyone can understand!

Diagram

Here’s how the Adapter Pattern works in practice - showing how incompatible interfaces are made compatible:

sequenceDiagram
    participant Client
    participant MediaPlayer as MediaPlayer Interface
    participant Adapter as MediaAdapter
    participant Mp4Player
    participant VlcPlayer
    
    Client->>MediaPlayer: play("mp4", "movie.mp4")
    activate MediaPlayer
    MediaPlayer->>Adapter: play("mp4", "movie.mp4")
    activate Adapter
    Adapter->>Adapter: Check audio_type
    Adapter->>Mp4Player: play_mp4("movie.mp4")
    activate Mp4Player
    Mp4Player-->>Adapter: Playing MP4
    deactivate Mp4Player
    Adapter-->>MediaPlayer: Success
    deactivate Adapter
    MediaPlayer-->>Client: Media playing
    deactivate MediaPlayer
    
    Note over Client,VlcPlayer: Adapter converts interface<br/>to make it compatible!
    
    Client->>MediaPlayer: play("vlc", "video.vlc")
    activate MediaPlayer
    MediaPlayer->>Adapter: play("vlc", "video.vlc")
    activate Adapter
    Adapter->>Adapter: Check audio_type
    Adapter->>VlcPlayer: play_vlc("video.vlc")
    activate VlcPlayer
    VlcPlayer-->>Adapter: Playing VLC
    deactivate VlcPlayer
    Adapter-->>MediaPlayer: Success
    deactivate Adapter
    MediaPlayer-->>Client: Media playing
    deactivate MediaPlayer

You’re building a media player application. You have an existing Mp3Player that works great, but you need to support Mp4Player and VlcPlayer which have different interfaces. Without Adapter Pattern:

bad_media_player.py
# ❌ Without Adapter Pattern - Incompatible interfaces!
class Mp3Player:
"""Existing MP3 player - works great!"""
def play_mp3(self, filename: str):
print(f"🎵 Playing MP3: {filename}")
class Mp4Player:
"""MP4 player - different interface!"""
def play_mp4(self, filename: str):
print(f"🎬 Playing MP4: {filename}")
class VlcPlayer:
"""VLC player - yet another interface!"""
def play_vlc(self, filename: str):
print(f"📺 Playing VLC: {filename}")
# Problem: Client needs to know about all different interfaces!
class AudioPlayer:
def play(self, audio_type: str, filename: str):
if audio_type == "mp3":
player = Mp3Player()
player.play_mp3(filename) # Different method name!
elif audio_type == "mp4":
player = Mp4Player()
player.play_mp4(filename) # Different method name!
elif audio_type == "vlc":
player = VlcPlayer()
player.play_vlc(filename) # Different method name!
else:
raise ValueError(f"Unsupported audio type: {audio_type}")
# Problems:
# - Client needs to know about all player types
# - Different method names for each player
# - Hard to add new players (need to modify AudioPlayer)
# - Violates Open/Closed Principle

Problems:

  • Client needs to know about all different player interfaces
  • Different method names for each player type
  • Hard to add new players - need to modify existing code
  • Violates Open/Closed Principle
classDiagram
    class MediaPlayer {
        <<interface>>
        +play(audio_type, filename) void
    }
    class Mp3Player {
        +play_mp3(filename) void
    }
    class Mp4Player {
        +play_mp4(filename) void
    }
    class VlcPlayer {
        +play_vlc(filename) void
    }
    class MediaAdapter {
        -mp4Player: Mp4Player
        -vlcPlayer: VlcPlayer
        +play(audio_type, filename) void
    }
    class AudioPlayer {
        -adapter: MediaAdapter
        +play(audio_type, filename) void
    }
    
    MediaPlayer <|.. Mp3Player : implements
    MediaPlayer <|.. MediaAdapter : implements
    MediaAdapter --> Mp4Player : adapts
    MediaAdapter --> VlcPlayer : adapts
    AudioPlayer --> MediaPlayer : uses
    
    note for MediaAdapter "Converts incompatible\ninterfaces to compatible ones"
    note for Mp4Player "Incompatible interface"
    note for VlcPlayer "Incompatible interface"
media_adapter.py
from abc import ABC, abstractmethod
# Step 1: Define the target interface (what client expects)
class MediaPlayer(ABC):
"""Target interface - what the client expects"""
@abstractmethod
def play(self, audio_type: str, filename: str) -> None:
"""Play media file"""
pass
# Step 2: Existing compatible class (no adapter needed)
class Mp3Player(MediaPlayer):
"""MP3 Player - already compatible with MediaPlayer interface"""
def play(self, audio_type: str, filename: str) -> None:
if audio_type == "mp3":
self.play_mp3(filename)
else:
raise ValueError(f"Mp3Player doesn't support {audio_type}")
def play_mp3(self, filename: str) -> None:
print(f"🎵 Playing MP3: {filename}")
# Step 3: Incompatible classes (need adapters)
class Mp4Player:
"""MP4 Player - incompatible interface!"""
def play_mp4(self, filename: str) -> None:
print(f"🎬 Playing MP4: {filename}")
class VlcPlayer:
"""VLC Player - incompatible interface!"""
def play_vlc(self, filename: str) -> None:
print(f"📺 Playing VLC: {filename}")
# Step 4: Create the Adapter
class MediaAdapter(MediaPlayer):
"""Adapter - makes incompatible interfaces compatible"""
def __init__(self):
self.mp4_player = Mp4Player()
self.vlc_player = VlcPlayer()
def play(self, audio_type: str, filename: str) -> None:
"""Adapt the incompatible interfaces to match MediaPlayer interface"""
if audio_type == "mp4":
# Convert play() call to play_mp4() call
self.mp4_player.play_mp4(filename)
elif audio_type == "vlc":
# Convert play() call to play_vlc() call
self.vlc_player.play_vlc(filename)
else:
raise ValueError(f"MediaAdapter doesn't support {audio_type}")
# Step 5: Client code - uses unified interface
class AudioPlayer:
"""Client - uses MediaPlayer interface"""
def __init__(self):
self.mp3_player = Mp3Player()
self.adapter = MediaAdapter()
def play(self, audio_type: str, filename: str) -> None:
"""Play media - unified interface for all types!"""
if audio_type == "mp3":
self.mp3_player.play(audio_type, filename)
elif audio_type in ["mp4", "vlc"]:
# Use adapter for incompatible players
self.adapter.play(audio_type, filename)
else:
raise ValueError(f"Unsupported audio type: {audio_type}")
# Usage - Clean and simple!
def main():
player = AudioPlayer()
# All use the same interface!
player.play("mp3", "song.mp3") # Direct - no adapter needed
player.play("mp4", "movie.mp4") # Uses adapter
player.play("vlc", "video.vlc") # Uses adapter
print("\n✅ All players work through unified interface!")
if __name__ == "__main__":
main()

Real-World Software Example: Payment Gateway Integration

Section titled “Real-World Software Example: Payment Gateway Integration”

Now let’s see a realistic software example - integrating multiple payment gateways with different interfaces into a unified payment system.

You’re building an e-commerce system that needs to support multiple payment gateways (Stripe, PayPal, Square). Each gateway has a different interface. Without Adapter Pattern:

bad_payments.py
# ❌ Without Adapter Pattern - Incompatible payment interfaces!
class StripePayment:
"""Stripe payment gateway - incompatible interface!"""
def charge_stripe(self, amount: float, card_token: str) -> dict:
# Stripe-specific implementation
return {"status": "success", "gateway": "stripe", "transaction_id": f"stripe_{amount}"}
class PayPalPayment:
"""PayPal payment gateway - different interface!"""
def process_paypal(self, amount: float, email: str) -> dict:
# PayPal-specific implementation
return {"status": "success", "gateway": "paypal", "transaction_id": f"paypal_{amount}"}
class SquarePayment:
"""Square payment gateway - yet another interface!"""
def make_payment(self, amount: float, payment_method: str) -> dict:
# Square-specific implementation
return {"status": "success", "gateway": "square", "transaction_id": f"square_{amount}"}
# Problem: Client needs to handle all different interfaces!
class PaymentProcessor:
def process_payment(self, gateway: str, amount: float, payment_data: dict) -> dict:
if gateway == "stripe":
stripe = StripePayment()
return stripe.charge_stripe(amount, payment_data["card_token"]) # Different method!
elif gateway == "paypal":
paypal = PayPalPayment()
return paypal.process_paypal(amount, payment_data["email"]) # Different method!
elif gateway == "square":
square = SquarePayment()
return square.make_payment(amount, payment_data["payment_method"]) # Different method!
else:
raise ValueError(f"Unsupported gateway: {gateway}")
# Problems:
# - Client needs to know about all gateway interfaces
# - Different method names and parameters for each gateway
# - Hard to add new gateways (need to modify PaymentProcessor)
# - Payment data format differs for each gateway

Problems:

  • Client needs to know about all different gateway interfaces
  • Different method names and parameters for each gateway
  • Hard to add new gateways - need to modify existing code
  • Payment data format differs for each gateway
payment_adapter.py
from abc import ABC, abstractmethod
from typing import Dict, Any
# Step 1: Define the target interface (what client expects)
class PaymentGateway(ABC):
"""Target interface - unified payment interface"""
@abstractmethod
def process_payment(self, amount: float, payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""Process payment - unified interface"""
pass
# Step 2: Incompatible payment gateway classes
class StripePayment:
"""Stripe payment gateway - incompatible interface!"""
def charge_stripe(self, amount: float, card_token: str) -> Dict[str, Any]:
# Stripe-specific implementation
print(f"💳 Processing Stripe payment: ${amount} with token {card_token[:10]}...")
return {
"status": "success",
"gateway": "stripe",
"transaction_id": f"stripe_{amount}",
"amount": amount
}
class PayPalPayment:
"""PayPal payment gateway - incompatible interface!"""
def process_paypal(self, amount: float, email: str) -> Dict[str, Any]:
# PayPal-specific implementation
print(f"💳 Processing PayPal payment: ${amount} for {email}...")
return {
"status": "success",
"gateway": "paypal",
"transaction_id": f"paypal_{amount}",
"amount": amount
}
class SquarePayment:
"""Square payment gateway - incompatible interface!"""
def make_payment(self, amount: float, payment_method: str) -> Dict[str, Any]:
# Square-specific implementation
print(f"💳 Processing Square payment: ${amount} with method {payment_method}...")
return {
"status": "success",
"gateway": "square",
"transaction_id": f"square_{amount}",
"amount": amount
}
# Step 3: Create Adapters for each incompatible gateway
class StripeAdapter(PaymentGateway):
"""Adapter for Stripe payment gateway"""
def __init__(self):
self.stripe = StripePayment()
def process_payment(self, amount: float, payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""Adapt Stripe interface to unified PaymentGateway interface"""
# Extract Stripe-specific data
card_token = payment_data.get("card_token")
if not card_token:
raise ValueError("Stripe requires 'card_token' in payment_data")
# Call Stripe's incompatible method
return self.stripe.charge_stripe(amount, card_token)
class PayPalAdapter(PaymentGateway):
"""Adapter for PayPal payment gateway"""
def __init__(self):
self.paypal = PayPalPayment()
def process_payment(self, amount: float, payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""Adapt PayPal interface to unified PaymentGateway interface"""
# Extract PayPal-specific data
email = payment_data.get("email")
if not email:
raise ValueError("PayPal requires 'email' in payment_data")
# Call PayPal's incompatible method
return self.paypal.process_paypal(amount, email)
class SquareAdapter(PaymentGateway):
"""Adapter for Square payment gateway"""
def __init__(self):
self.square = SquarePayment()
def process_payment(self, amount: float, payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""Adapt Square interface to unified PaymentGateway interface"""
# Extract Square-specific data
payment_method = payment_data.get("payment_method")
if not payment_method:
raise ValueError("Square requires 'payment_method' in payment_data")
# Call Square's incompatible method
return self.square.make_payment(amount, payment_method)
# Step 4: Client code - uses unified interface
class PaymentService:
"""Client - uses unified PaymentGateway interface"""
def __init__(self):
self.adapters = {
"stripe": StripeAdapter(),
"paypal": PayPalAdapter(),
"square": SquareAdapter()
}
def process_payment(self, gateway: str, amount: float, payment_data: Dict[str, Any]) -> Dict[str, Any]:
"""Process payment - unified interface for all gateways!"""
adapter = self.adapters.get(gateway.lower())
if not adapter:
raise ValueError(f"Unsupported payment gateway: {gateway}")
# All gateways use the same interface!
return adapter.process_payment(amount, payment_data)
# Usage - Clean and unified!
def main():
payment_service = PaymentService()
# All gateways use the same interface!
result1 = payment_service.process_payment(
"stripe",
100.0,
{"card_token": "tok_1234567890"}
)
print(f"✅ {result1}\n")
result2 = payment_service.process_payment(
"paypal",
50.0,
{"email": "customer@example.com"}
)
print(f"✅ {result2}\n")
result3 = payment_service.process_payment(
"square",
75.0,
{"payment_method": "card_1234"}
)
print(f"✅ {result3}\n")
print("🎉 All payments processed through unified interface!")
if __name__ == "__main__":
main()

There are two main ways to implement the Adapter Pattern:

Uses composition - adapter contains an instance of the adaptee:

object_adapter.py
# Object Adapter - uses composition
class Target:
def request(self):
return "Target request"
class Adaptee:
def specific_request(self):
return "Adaptee specific request"
class Adapter(Target):
def __init__(self):
self.adaptee = Adaptee() # Composition
def request(self):
# Adapt the interface
return self.adaptee.specific_request()

Pros: More flexible, can adapt multiple adaptees
Cons: Requires adaptee instance

Uses inheritance - adapter extends both target and adaptee:

class_adapter.py
# Class Adapter - uses multiple inheritance (Python supports this)
class Target:
def request(self):
return "Target request"
class Adaptee:
def specific_request(self):
return "Adaptee specific request"
class Adapter(Target, Adaptee): # Multiple inheritance
def request(self):
# Adapt the interface
return self.specific_request()

Pros: No need for adaptee instance
Cons: Less flexible, language-dependent (Java doesn’t support multiple inheritance)


Use Adapter Pattern when:

Integrating third-party libraries - Library interface doesn’t match your code
Legacy code integration - Old code needs to work with new code
Multiple incompatible interfaces - Need to make them work together
You want to avoid modifying - Don’t want to change existing code
Unified interface needed - Want consistent interface for similar functionality

Don’t use Adapter Pattern when:

You can modify the code - If you can change the incompatible interface, do that instead
Simple interface mismatch - If mismatch is trivial, direct modification might be better
Over-engineering - Don’t add complexity for simple cases
Performance critical - Adapter adds a layer of indirection (usually negligible)


Mistake 1: Modifying Existing Code Instead of Adapting

Section titled “Mistake 1: Modifying Existing Code Instead of Adapting”
modifying_code.py
# ❌ Bad: Modifying existing code to make it compatible
class ThirdPartyLibrary:
def old_method(self, param):
return f"Result: {param}"
# Modifying third-party code - risky!
class ThirdPartyLibrary:
def old_method(self, param):
return f"Result: {param}"
def new_method(self, param): # Added method - but you shouldn't modify third-party code!
return self.old_method(param)
# ✅ Good: Use Adapter instead
class Adapter:
def __init__(self):
self.library = ThirdPartyLibrary() # Use as-is
def new_method(self, param):
return self.library.old_method(param) # Adapt the interface

Mistake 2: Creating Adapters for Simple Cases

Section titled “Mistake 2: Creating Adapters for Simple Cases”
over_engineering.py
# ❌ Bad: Creating adapter for trivial case
class SimpleClass:
def method(self, x):
return x * 2
class UnnecessaryAdapter:
def __init__(self):
self.obj = SimpleClass()
def method(self, x):
return self.obj.method(x) # Just passes through - unnecessary!
# ✅ Better: Use the class directly
obj = SimpleClass()
result = obj.method(5) # Simple and direct!

Mistake 3: Not Handling Data Transformation

Section titled “Mistake 3: Not Handling Data Transformation”
data_transformation.py
# ❌ Bad: Not transforming data properly
class Adapter:
def __init__(self):
self.adaptee = Adaptee()
def request(self, data):
# Problem: Not transforming data format!
return self.adaptee.specific_request(data) # Data format might not match!
# ✅ Good: Transform data properly
class Adapter:
def __init__(self):
self.adaptee = Adaptee()
def request(self, data):
# Transform data to match adaptee's expected format
transformed_data = self._transform(data)
return self.adaptee.specific_request(transformed_data)
def _transform(self, data):
# Transform data format
return {"old_format": data}

  1. Reusability - Use existing classes without modification
  2. Flexibility - Easy to add new adapters for new incompatible classes
  3. Separation of Concerns - Adapter handles compatibility, client stays clean
  4. Open/Closed Principle - Open for extension (new adapters), closed for modification
  5. Legacy Integration - Integrate old code with new code easily

Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces.

  • Integrate incompatible interfaces - Make old and new code work together
  • Use third-party libraries - Library interface doesn’t match your code
  • Avoid modifying code - Don’t want to change existing working code
  • Unified interface - Want consistent interface for similar functionality
  • Legacy integration - Integrate legacy systems with new systems
  1. Define target interface - What the client expects
  2. Identify adaptee - The incompatible class
  3. Create adapter - Implements target interface, wraps adaptee
  4. Transform calls - Adapter converts target calls to adaptee calls
  5. Client uses adapter - Client uses unified interface
Client → Target Interface → Adapter → Adaptee
  • Target Interface - What the client expects
  • Adaptee - The incompatible class
  • Adapter - Bridges target and adaptee
  • Client - Uses the target interface
class Target:
def request(self): pass
class Adaptee:
def specific_request(self): pass
class Adapter(Target):
def __init__(self):
self.adaptee = Adaptee()
def request(self):
return self.adaptee.specific_request() # Adapt!

✅ Integrating third-party libraries
✅ Legacy code integration
✅ Multiple incompatible interfaces
✅ Want to avoid modifying existing code
✅ Need unified interface

❌ You can modify the code directly
❌ Simple interface mismatch
❌ Over-engineering simple cases
❌ Performance critical (adds indirection)

  • Adapter Pattern = Makes incompatible interfaces compatible
  • Target = Interface client expects
  • Adaptee = Incompatible class
  • Adapter = Bridge between target and adaptee
  • Benefit = Reuse code without modification
  • Use Case = Third-party libraries, legacy integration
class Target:
def request(self): pass
class Adaptee:
def specific_request(self): pass
class Adapter(Target):
def __init__(self):
self.adaptee = Adaptee()
def request(self):
return self.adaptee.specific_request()
# Usage
adapter = Adapter()
adapter.request() # Works!
  • Adapter Pattern bridges incompatible interfaces
  • It allows reuse without modification
  • Use Object Adapter (composition) in most cases
  • Don’t use it for simple cases - avoid over-engineering
  • It’s about compatibility, not just wrapping!

What to say:

“Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, allowing them to collaborate without modifying their source code.”

Why it matters:

  • Shows you understand the fundamental purpose
  • Demonstrates knowledge of when to use it
  • Indicates you can explain concepts clearly

Must mention:

  • Integrating third-party libraries - Library interface doesn’t match your code
  • Legacy code integration - Old code needs to work with new code
  • Multiple incompatible interfaces - Need to make them work together
  • Avoid modifying code - Don’t want to change existing code

Example scenario to give:

“I’d use Adapter Pattern when integrating a third-party payment gateway like Stripe into my e-commerce system. Stripe has its own interface (charge_stripe()), but my system expects a unified payment interface (process_payment()). I create a StripeAdapter that implements my payment interface and internally calls Stripe’s methods.”

Must discuss:

  • Object Adapter: Uses composition, more flexible
  • Class Adapter: Uses inheritance, language-dependent
  • Preference: Object Adapter is preferred in most cases

Example to give:

“I prefer Object Adapter because it uses composition, making it more flexible. I can adapt multiple adaptees or change behavior at runtime. Class Adapter uses inheritance, which is less flexible and doesn’t work well in languages like Java that don’t support multiple inheritance.”

Benefits to mention:

  • Reusability - Use existing classes without modification
  • Flexibility - Easy to add new adapters
  • Separation of Concerns - Adapter handles compatibility
  • Open/Closed Principle - Open for extension, closed for modification

Trade-offs to acknowledge:

  • Complexity - Adds a layer of indirection
  • Performance - Small overhead (usually negligible)
  • Over-engineering risk - Can be overkill for simple cases

Q: “What’s the difference between Adapter Pattern and Decorator Pattern?”

A:

“Adapter Pattern changes the interface of an object to make it compatible with another interface. Decorator Pattern adds new behavior to an object without changing its interface. Adapter is about compatibility, Decorator is about adding functionality.”

Q: “When would you use Adapter vs modifying the code directly?”

A:

“I use Adapter when I can’t or shouldn’t modify the code - like third-party libraries, legacy systems, or when modifying would break existing functionality. If I own the code and modification is safe, I might modify directly. But Adapter is safer and follows Open/Closed Principle.”

Q: “How does Adapter Pattern relate to SOLID principles?”

A:

“Adapter Pattern primarily supports the Open/Closed Principle - you can extend functionality (add new adapters) without modifying existing code. It also supports Single Responsibility Principle by separating the adaptation logic into its own class. Additionally, it helps with Dependency Inversion Principle by allowing clients to depend on abstractions (target interface) rather than concrete implementations.”

Before your interview, make sure you can:

  • Define Adapter Pattern clearly in one sentence
  • Explain when to use it (with examples)
  • Describe Object Adapter vs Class Adapter
  • Implement Adapter Pattern from scratch
  • Compare with other structural patterns (Decorator, Facade)
  • List benefits and trade-offs
  • Identify common mistakes
  • Give 2-3 real-world examples
  • Connect to SOLID principles
  • Discuss when NOT to use it

Remember: Adapter Pattern is about making incompatible interfaces work together - bridging the gap between old and new, third-party and your code! 🔌