Adapter Pattern: Structural Design Guide
TL;DR
The Adapter Pattern converts the interface of one class into another interface that clients expect. It allows incompatible classes to work together by wrapping one class with an adapter that translates method calls. Think of it like a power adapter that lets you plug a US device into a European outlet.
Core Concept
What is the Adapter Pattern?
The Adapter Pattern is a structural design pattern that acts as a bridge between two incompatible interfaces. When you have existing code that expects one interface but you need to use a class with a different interface, an adapter wraps the incompatible class and translates calls between them.
Why It Matters
You’ll encounter adapters constantly in real-world development:
- Integrating third-party libraries with different APIs
- Working with legacy code that can’t be modified
- Making multiple payment gateways work with your checkout system
- Connecting new features to old systems without breaking existing code
The adapter pattern follows the Open/Closed Principle: you extend functionality without modifying existing code.
Two Types of Adapters
Object Adapter (Composition)
The adapter contains an instance of the class it adapts. This is more flexible because you can adapt multiple classes and change the adaptee at runtime.
Class Adapter (Inheritance)
The adapter inherits from the class it adapts. This only works in languages supporting multiple inheritance (like C++ and Python). It’s less flexible but slightly more efficient.
Most implementations use object adapters because composition is more flexible than inheritance.
Key Components
- Target Interface: The interface your client code expects
- Adaptee: The existing class with an incompatible interface
- Adapter: The class that implements the target interface and translates calls to the adaptee
- Client: Code that uses the target interface
When to Use It
- You want to use an existing class but its interface doesn’t match what you need
- You need to create a reusable class that cooperates with unrelated classes
- You’re integrating multiple third-party libraries with different interfaces
- You need to use several existing subclasses but can’t adapt their interface by subclassing each one
Visual Guide
Adapter Pattern Structure
classDiagram
class Client {
+request()
}
class Target {
<<interface>>
+request()
}
class Adapter {
-adaptee: Adaptee
+request()
}
class Adaptee {
+specific_request()
}
Client --> Target
Adapter ..|> Target
Adapter --> Adaptee
note for Adapter "Translates request()\nto specific_request()"
The Adapter implements the Target interface and delegates to the Adaptee, translating method calls between incompatible interfaces.
Real-World Example: Payment Processing
classDiagram
class CheckoutSystem {
+process_payment(amount)
}
class PaymentProcessor {
<<interface>>
+pay(amount)
}
class StripeAdapter {
-stripe: StripeAPI
+pay(amount)
}
class PayPalAdapter {
-paypal: PayPalAPI
+pay(amount)
}
class StripeAPI {
+create_charge(cents, currency)
}
class PayPalAPI {
+send_payment(dollars, email)
}
CheckoutSystem --> PaymentProcessor
StripeAdapter ..|> PaymentProcessor
PayPalAdapter ..|> PaymentProcessor
StripeAdapter --> StripeAPI
PayPalAdapter --> PayPalAPI
Multiple payment gateways with different APIs are adapted to work with a single PaymentProcessor interface.
Examples
Example 1: Media Player Adapter
Imagine you have a media player that only plays MP3 files, but you want to add support for MP4 and VLC formats using third-party libraries.
# Target interface - what our client expects
class MediaPlayer:
def play(self, audio_type: str, filename: str):
pass
# Adaptee - existing class with incompatible interface
class AdvancedMediaPlayer:
def play_mp4(self, filename: str):
print(f"Playing MP4 file: {filename}")
def play_vlc(self, filename: str):
print(f"Playing VLC file: {filename}")
# Adapter - makes AdvancedMediaPlayer compatible with MediaPlayer
class MediaAdapter(MediaPlayer):
def __init__(self, audio_type: str):
self.audio_type = audio_type
self.advanced_player = AdvancedMediaPlayer()
def play(self, audio_type: str, filename: str):
if audio_type == "mp4":
self.advanced_player.play_mp4(filename)
elif audio_type == "vlc":
self.advanced_player.play_vlc(filename)
# Concrete implementation using the adapter
class AudioPlayer(MediaPlayer):
def play(self, audio_type: str, filename: str):
# Built-in support for MP3
if audio_type == "mp3":
print(f"Playing MP3 file: {filename}")
# Use adapter for other formats
elif audio_type in ["mp4", "vlc"]:
adapter = MediaAdapter(audio_type)
adapter.play(audio_type, filename)
else:
print(f"Invalid format: {audio_type}")
# Client code
player = AudioPlayer()
player.play("mp3", "song.mp3") # Output: Playing MP3 file: song.mp3
player.play("mp4", "video.mp4") # Output: Playing MP4 file: video.mp4
player.play("vlc", "movie.vlc") # Output: Playing VLC file: movie.vlc
player.play("avi", "clip.avi") # Output: Invalid format: avi
Try it yourself: Add support for a new format (like AVI) by extending the AdvancedMediaPlayer and MediaAdapter classes.
Example 2: Temperature Sensor Adapter
You have a weather monitoring system that expects temperatures in Celsius, but you need to integrate a legacy sensor that only reports in Fahrenheit.
from abc import ABC, abstractmethod
# Target interface
class TemperatureSensor(ABC):
@abstractmethod
def get_temperature_celsius(self) -> float:
pass
# Adaptee - legacy sensor with incompatible interface
class FahrenheitSensor:
def __init__(self, location: str):
self.location = location
def get_temp_fahrenheit(self) -> float:
# Simulate reading from hardware
return 72.5 # Fahrenheit
def get_location(self) -> str:
return self.location
# Adapter using composition (object adapter)
class FahrenheitTocelsius Adapter(TemperatureSensor):
def __init__(self, fahrenheit_sensor: FahrenheitSensor):
self.sensor = fahrenheit_sensor
def get_temperature_celsius(self) -> float:
# Convert Fahrenheit to Celsius
fahrenheit = self.sensor.get_temp_fahrenheit()
celsius = (fahrenheit - 32) * 5 / 9
return round(celsius, 2)
def get_location(self) -> str:
# Pass through other methods if needed
return self.sensor.get_location()
# Modern sensor already implements the target interface
class CelsiusSensor(TemperatureSensor):
def __init__(self, location: str):
self.location = location
def get_temperature_celsius(self) -> float:
return 22.5 # Already in Celsius
# Weather monitoring system (client)
class WeatherStation:
def __init__(self):
self.sensors: list[TemperatureSensor] = []
def add_sensor(self, sensor: TemperatureSensor):
self.sensors.append(sensor)
def get_average_temperature(self) -> float:
if not self.sensors:
return 0.0
total = sum(sensor.get_temperature_celsius() for sensor in self.sensors)
return round(total / len(self.sensors), 2)
# Client code
station = WeatherStation()
# Add modern sensor
modern_sensor = CelsiusSensor("Room A")
station.add_sensor(modern_sensor)
# Add legacy sensor through adapter
legacy_sensor = FahrenheitSensor("Room B")
adapted_sensor = FahrenheitToCelsiusAdapter(legacy_sensor)
station.add_sensor(adapted_sensor)
print(f"Average temperature: {station.get_average_temperature()}°C")
# Output: Average temperature: 22.75°C
# (22.5 + 22.5 from converted 72.5°F) / 2
print(f"Legacy sensor location: {adapted_sensor.get_location()}")
# Output: Legacy sensor location: Room B
Expected Output:
Average temperature: 22.75°C
Legacy sensor location: Room B
Try it yourself: Create a KelvinSensor class and an adapter to make it work with the WeatherStation.
Java/C++ Notes
Java: Uses interfaces explicitly. The adapter implements the target interface and holds a reference to the adaptee.
// Target interface
interface MediaPlayer {
void play(String audioType, String filename);
}
// Adapter
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
this.advancedPlayer = new AdvancedMediaPlayer();
}
@Override
public void play(String audioType, String filename) {
if (audioType.equals("mp4")) {
advancedPlayer.playMp4(filename);
}
}
}
C++: Can use multiple inheritance for class adapters, but object adapters (composition) are still preferred.
// Object adapter (preferred)
class MediaAdapter : public MediaPlayer {
private:
AdvancedMediaPlayer* advancedPlayer;
public:
MediaAdapter() {
advancedPlayer = new AdvancedMediaPlayer();
}
void play(string audioType, string filename) override {
advancedPlayer->playMp4(filename);
}
};
Common Mistakes
1. Overusing Adapters When You Should Refactor
Mistake: Creating adapters for every minor interface difference instead of refactoring the code.
# Bad - adapter for trivial difference
class CalculatorAdapter:
def __init__(self, calc):
self.calc = calc
def add(self, a, b):
return self.calc.sum(a, b) # Just renaming sum to add
Why it’s wrong: If you control both interfaces, refactor them to match. Adapters are for code you can’t change (third-party libraries, legacy systems).
Fix: Only use adapters when you truly can’t modify the adaptee or when multiple clients depend on different interfaces.
2. Exposing Adaptee Implementation Details
Mistake: Letting the adapter leak implementation details of the adaptee to clients.
# Bad - exposing adaptee's methods
class PaymentAdapter:
def __init__(self, stripe_api):
self.stripe = stripe_api
def pay(self, amount):
return self.stripe.create_charge(amount * 100, "usd")
# Don't do this - exposes Stripe specifics
def get_stripe_customer_id(self):
return self.stripe.customer_id
Why it’s wrong: Clients become dependent on Stripe’s API, defeating the purpose of the adapter.
Fix: Only expose methods defined in the target interface. Keep adaptee details private.
3. Creating Two-Way Adapters
Mistake: Making an adapter that translates in both directions.
# Bad - bidirectional adapter
class BidirectionalAdapter(InterfaceA, InterfaceB):
def method_a(self):
return self.method_b() # A calls B
def method_b(self):
return self.method_a() # B calls A - circular!
Why it’s wrong: Creates circular dependencies and confusion about which interface is the target. Adapters should have a clear direction: from adaptee to target.
Fix: Create two separate adapters if you need bidirectional translation, or reconsider your design.
4. Forgetting to Handle Exceptions
Mistake: Not translating exceptions from the adaptee to match what clients expect.
# Bad - lets adaptee exceptions leak through
class DatabaseAdapter:
def __init__(self, legacy_db):
self.db = legacy_db
def query(self, sql):
# Throws LegacyDBException that clients don't know about
return self.db.execute_query(sql)
Why it’s wrong: Clients must handle exceptions from the adaptee, creating coupling.
Fix: Catch adaptee exceptions and translate them to exceptions the target interface specifies.
# Good - translates exceptions
class DatabaseAdapter:
def query(self, sql):
try:
return self.db.execute_query(sql)
except LegacyDBException as e:
raise DatabaseError(f"Query failed: {e}")
5. Not Considering Performance Implications
Mistake: Creating adapters that add significant overhead through unnecessary conversions.
# Bad - converts data multiple times
class DataAdapter:
def process(self, data: list) -> list:
# Convert to dict, then to JSON, then back to dict, then to list
dict_data = {i: v for i, v in enumerate(data)}
json_str = json.dumps(dict_data)
parsed = json.loads(json_str)
return list(parsed.values())
Why it’s wrong: Unnecessary conversions waste CPU and memory, especially with large datasets.
Fix: Minimize conversions. Cache results if the adaptee is expensive to call. Profile your adapter if performance matters.
Interview Tips
What Interviewers Look For
1. Pattern Recognition: Can you identify when to use an adapter versus other patterns?
- Bridge Pattern: Separates abstraction from implementation (both can vary independently)
- Decorator Pattern: Adds functionality without changing interface
- Adapter Pattern: Changes interface to match what client expects
Be ready to explain: “I’d use an adapter here because we need to integrate this third-party library that has a different interface than our existing code expects.”
2. Implementation Choices: Explain why you chose object adapter over class adapter.
Good answer: “I used composition (object adapter) because it’s more flexible—I can adapt multiple classes and swap the adaptee at runtime. Class adapters using inheritance are less flexible and don’t work well in languages without multiple inheritance.”
3. Real-World Application: Describe concrete scenarios where you’ve used or would use adapters.
Strong examples:
- “Integrating multiple payment gateways (Stripe, PayPal, Square) behind a single PaymentProcessor interface”
- “Wrapping legacy database code to work with a modern ORM”
- “Adapting different logging libraries to a common logging interface”
Common Interview Questions
Q: How does the Adapter pattern differ from the Facade pattern?
Answer: An adapter converts one interface to another that clients expect. A facade provides a simplified interface to a complex subsystem. Adapters focus on interface compatibility; facades focus on simplification. You might use both together—a facade that uses adapters internally.
Q: When would you NOT use an adapter?
Answer: Don’t use adapters when:
- You control both interfaces and can refactor them to match
- The interface difference is trivial (just renaming)
- You’re adding functionality, not just translating (use Decorator instead)
- You need to change behavior significantly (consider Strategy pattern)
Q: Can you implement this adapter on the whiteboard?
Approach:
- Draw the target interface first
- Draw the adaptee with its incompatible interface
- Create the adapter class implementing the target interface
- Show the adapter holding a reference to the adaptee
- Implement one method showing the translation
Code Challenge Practice
Typical problem: “You have a Rectangle class with width and height properties. Create an adapter so it works with code expecting a Shape interface with area() and perimeter() methods.”
Key points to demonstrate:
- Define the target interface clearly
- Use composition to hold the adaptee
- Implement translation logic correctly
- Handle edge cases (negative dimensions, etc.)
- Explain your design choices
Red Flags to Avoid
- Saying “adapter” and “wrapper” are the same (wrappers are more general)
- Not knowing the difference between object and class adapters
- Suggesting adapters when the real problem is poor initial design
- Creating adapters that do more than interface translation (mixing concerns)
Key Takeaways
- The Adapter Pattern converts one interface to another, allowing incompatible classes to work together without modifying their source code.
- Use composition (object adapter) over inheritance (class adapter) for flexibility—you can adapt multiple classes and change the adaptee at runtime.
- Adapters are for integration, not refactoring—only use them when you can’t change the adaptee (third-party libraries, legacy code) or when multiple clients need different interfaces.
- Keep adapters focused on interface translation—don’t add business logic, expose adaptee details, or create bidirectional adapters.
- In interviews, demonstrate pattern recognition by explaining when to use adapters versus facades, decorators, or bridges, and provide concrete real-world examples like payment gateway integration.