Adapter Pattern: Structural Design Guide

Updated 2026-03-11

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.

Prerequisites: Understanding of classes and objects, interfaces (or abstract base classes in Python), inheritance and composition, basic polymorphism concepts.

After this topic: Implement adapter classes to make incompatible interfaces work together, identify when to use adapters versus other patterns, design both class-based and object-based adapters, and explain the trade-offs between adapter approaches in technical interviews.

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

  1. Target Interface: The interface your client code expects
  2. Adaptee: The existing class with an incompatible interface
  3. Adapter: The class that implements the target interface and translates calls to the adaptee
  4. 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:

  1. Draw the target interface first
  2. Draw the adaptee with its incompatible interface
  3. Create the adapter class implementing the target interface
  4. Show the adapter holding a reference to the adaptee
  5. 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.