Chapter 04 · Article 14 of 55
Adapter Pattern
Intent: Convert the interface of a class into another interface that clients expect. The Adapter pattern allows classes with incompatible interfaces to work together.
Article outline15 sections on this page+
Overview
Intent: Convert the interface of a class into another interface that clients expect. The Adapter pattern allows classes with incompatible interfaces to work together.
Also Known As: Wrapper
Category: Structural Design Pattern
GoF Classification: Object Structural (primary), Class Structural (via multiple inheritance)
The Adapter pattern acts as a bridge between two incompatible interfaces. Just as a physical power adapter lets a US plug work in a European socket, a software adapter translates calls from one interface into calls understood by another. The client programs against the Target interface, completely unaware that an Adapter is translating requests to an Adaptee behind the scenes.
Core Principle: Program to an interface, not an implementation - then adapt what doesn't fit.
Problem It Solves
Software systems frequently encounter interface incompatibilities:
-
Legacy Integration - A modern system needs to consume a legacy component whose API predates current conventions. Rewriting the legacy code is risky or impossible (no source access, regulatory freeze, cost).
-
Third-Party Library Wrapping - You depend on a vendor library whose interface differs from your domain abstractions. Coupling directly to the vendor locks you in; an adapter isolates the dependency.
-
Interface Mismatch After Refactoring - Two subsystems evolved independently and now need to collaborate, but their method signatures, data formats, or protocols diverge.
-
Testing & Mocking - Wrapping an external service behind an adapter interface enables substitution with test doubles.
-
Multi-Vendor Support - Supporting multiple payment gateways, cloud providers, or analytics engines that each expose different APIs behind a single unified interface.
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| You need to use an existing class but its interface doesn't match what you need | The interfaces are already compatible - adding an adapter is unnecessary indirection |
| You want to create a reusable class that cooperates with unrelated or unforeseen classes | You need to add new behavior or responsibilities (use Decorator instead) |
| You need to integrate a third-party library without coupling your code to it | You want to simplify a complex subsystem (use Facade instead) |
| You're migrating from one implementation to another and need a transitional layer | The adaptee is unstable and changes frequently - the adapter becomes a maintenance burden |
| You want to isolate legacy code behind a clean interface for testability | You control both interfaces and can simply refactor one to match the other |
| Multiple classes need to be adapted to a common interface (e.g., multi-vendor) | Performance is ultra-critical and the extra indirection layer is unacceptable |
Key Concepts & Theory
Object Adapter vs Class Adapter
- Object Adapter uses composition: the adapter holds a reference to the adaptee and delegates calls. This is the preferred approach in languages without multiple inheritance (Java, C#, Go, TypeScript).
- Class Adapter uses multiple inheritance: the adapter inherits from both the Target interface and the Adaptee class simultaneously. Available in C++ and Python but not in Java/C#.
Two-Way Adapters
A two-way adapter implements both interfaces, allowing it to be used as either type. This is useful when two subsystems need to call each other and both expect their own interface. The adapter translates in both directions.
Wrapper Terminology
"Wrapper" is an informal synonym for Adapter, but the term is overloaded - Decorator and Facade are also sometimes called wrappers. The distinction:
- Adapter wraps to change the interface
- Decorator wraps to add behavior without changing the interface
- Facade wraps to simplify a complex subsystem interface
Participants
| Participant | Role |
|---|---|
| Target | The interface the client expects |
| Adapter | Translates calls from Target interface to Adaptee interface |
| Adaptee | The existing class with an incompatible interface |
| Client | Collaborates with objects conforming to the Target interface |
ASCII Class Diagram
Object Adapter (Composition)
┌────────────┐ ┌─────────────────────┐
│ Client │────────▶│ <<interface>> │
└────────────┘ │ Target │
├─────────────────────┤
│ + request() │
└─────────┬───────────┘
│ implements
│
┌─────────┴───────────┐ ┌─────────────────────┐
│ Adapter │──────▶│ Adaptee │
├─────────────────────┤ has-a ├─────────────────────┤
│ - adaptee: Adaptee │ │ + specificRequest() │
├─────────────────────┤ └─────────────────────┘
│ + request() │
│ // delegates to │
│ // adaptee │
└─────────────────────┘
Class Adapter (Multiple Inheritance)
┌────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Client │────────▶│ <<interface>> │ │ Adaptee │
└────────────┘ │ Target │ ├─────────────────────┤
├─────────────────────┤ │ + specificRequest() │
│ + request() │ └──────────┬──────────┘
└─────────┬───────────┘ │
│ implements │ inherits
│ │
┌─────────┴────────────────────────────┴──┐
│ Adapter │
├─────────────────────────────────────────┤
│ + request() │
│ // calls inherited specificRequest() │
└─────────────────────────────────────────┘
Pseudocode Implementation
Example 1: Legacy XML Analytics → JSON Interface
A modern dashboard expects analytics data in JSON format, but the legacy analytics engine only outputs XML.
# === Target Interface ===
class AnalyticsProvider:
"""Interface the client expects"""
def get_report(self, report_id: str) -> dict:
"""Returns analytics data as a JSON-compatible dict"""
raise NotImplementedError
# === Adaptee (Legacy System) ===
class LegacyXmlAnalyticsEngine:
"""Legacy library we cannot modify - returns XML strings"""
def generate_xml_report(self, report_id: str) -> str:
# Simulates legacy behavior
return f"<report><id>{report_id}</id><visits>42000</visits><bounce>0.35</bounce></report>"
# === Adapter ===
class XmlToJsonAnalyticsAdapter(AnalyticsProvider):
"""Adapts the legacy XML engine to the JSON-based Target interface"""
def __init__(self, legacy_engine: LegacyXmlAnalyticsEngine):
self._engine = legacy_engine # composition - object adapter
def get_report(self, report_id: str) -> dict:
xml_data = self._engine.generate_xml_report(report_id)
return self._parse_xml_to_dict(xml_data)
def _parse_xml_to_dict(self, xml: str) -> dict:
# Simplified XML parsing for illustration
import xml.etree.ElementTree as ET
root = ET.fromstring(xml)
return {child.tag: child.text for child in root}
# === Client Code ===
def render_dashboard(provider: AnalyticsProvider):
data = provider.get_report("RPT-2026-Q1")
print(f"Visits: {data['visits']}, Bounce Rate: {data['bounce']}")
# Usage - client is decoupled from the legacy engine
legacy = LegacyXmlAnalyticsEngine()
adapter = XmlToJsonAnalyticsAdapter(legacy)
render_dashboard(adapter)
Example 2: Power Socket Adapter Analogy
# === Target Interface ===
class EuropeanSocket:
"""European standard: 220V, round pins"""
def provide_power_round_pin(self) -> str:
raise NotImplementedError
# === Adaptee ===
class USAppliance:
"""US appliance with flat-pin plug, expects 120V"""
def plug_flat_pin(self) -> str:
return "Receiving 120V via flat pins"
# === Adapter ===
class USToEuropeanAdapter(EuropeanSocket):
"""Converts European round-pin socket interface for a US flat-pin appliance"""
def __init__(self, us_appliance: USAppliance):
self._appliance = us_appliance
def provide_power_round_pin(self) -> str:
# Adapter handles voltage conversion + pin shape translation
return f"[Adapter: converting 220V→120V, round→flat] → {self._appliance.plug_flat_pin()}"
# === Client ===
def power_on(socket: EuropeanSocket):
print(socket.provide_power_round_pin())
us_device = USAppliance()
adapter = USToEuropeanAdapter(us_device)
power_on(adapter)
# Output: [Adapter: converting 220V→120V, round→flat] → Receiving 120V via flat pins
Object Adapter vs Class Adapter
| Criterion | Object Adapter (Composition) | Class Adapter (Inheritance) |
|---|---|---|
| Mechanism | Holds reference to adaptee instance | Inherits from both Target and Adaptee |
| Language Support | All OOP languages | Only languages with multiple inheritance (C++, Python) |
| Flexibility | Can adapt any subclass of Adaptee at runtime | Bound to one specific Adaptee class at compile time |
| Override Behavior | Cannot override Adaptee methods directly | Can override Adaptee methods (more control) |
| Coupling | Loose - depends on Adaptee's public interface | Tight - inherits all of Adaptee's internals |
| Multiple Adaptees | Can compose multiple adaptees in one adapter | Requires multiple inheritance from all adaptees |
| SOLID Compliance | Favors Single Responsibility and Open/Closed | May violate SRP by mixing two class hierarchies |
| Testability | Easy to mock the adaptee | Harder to isolate in tests |
| Recommendation | Preferred in most scenarios | Use only when you need to override adaptee behavior and language supports it |
Real-World Examples
1. JDBC Drivers (Java)
JDBC defines a standard Connection, Statement, ResultSet interface (Target). Each database vendor (MySQL, PostgreSQL, Oracle) provides a driver that adapts their proprietary wire protocol (Adaptee) to the JDBC interface. Application code programs against JDBC - never against vendor-specific classes.
2. SLF4J Logging Facade
SLF4J defines a logging Target interface. Adapters (slf4j-log4j12, slf4j-jdk14, logback-classic) translate SLF4J calls to the underlying logging framework. You can swap Log4j for Logback without changing application code.
3. Payment Gateway Adapters
An e-commerce platform defines a PaymentProcessor interface with charge(), refund(), authorize(). Adapters wrap Stripe, PayPal, and Adyen SDKs - each with different method names, authentication flows, and response formats - behind this unified interface.
4. File Format Converters
A document management system expects Document.render() → PDF. Adapters wrap libraries for DOCX, Markdown, HTML, and LaTeX, each with different rendering APIs, converting them to the common Document interface.
5. Java's Arrays.asList()
Arrays.asList() adapts a raw array (Adaptee) into the List interface (Target), allowing array data to be used wherever List is expected.
Adapter vs Facade vs Decorator
| Aspect | Adapter | Facade | Decorator |
|---|---|---|---|
| Intent | Make incompatible interfaces compatible | Simplify a complex subsystem | Add responsibilities dynamically |
| Interface Change | Converts one interface to another | Defines a new simplified interface | Keeps the same interface |
| Number of Wrapped Objects | Usually one | Multiple subsystem objects | One |
| Client Awareness | Client uses Target interface, unaware of Adaptee | Client uses Facade, unaware of subsystem complexity | Client uses same interface, unaware of added behavior |
| Structural Change | Changes interface shape | Reduces interface surface area | Preserves interface, adds layers |
| Typical Use Case | Legacy integration, third-party wrapping | Providing a simple API over a library | Adding logging, caching, validation transparently |
| Composition Depth | Single level | Single level over many objects | Can be nested (stacked decorators) |
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Single Responsibility - separates interface conversion from business logic | Increased complexity - adds extra classes and indirection |
| Open/Closed Principle - introduce new adapters without modifying existing code | Performance overhead - extra delegation layer (usually negligible) |
| Decoupling - client is isolated from third-party/legacy changes | Debugging difficulty - call stack is deeper; harder to trace through adapters |
| Testability - easy to mock the Target interface in tests | Over-engineering risk - unnecessary when interfaces are already compatible |
| Reusability - same adapter works for any client expecting the Target | Data loss risk - if Adaptee's interface is richer, adapter may discard information |
| Gradual migration - enables incremental replacement of legacy systems | Maintenance burden - adapter must be updated when either interface changes |
Constraints & Edge Cases
Adapting Multiple Interfaces
When an adapter must bridge multiple incompatible interfaces simultaneously, complexity grows. Solutions:
- Compose multiple adaptees within a single adapter (object adapter approach)
- Use the Abstract Factory pattern to produce families of related adapters
- Consider whether a Facade is more appropriate if you're simplifying rather than translating
Performance Overhead
Each adapter call adds a method invocation and potential data transformation. In hot paths (millions of calls/sec), consider:
- Caching transformed results
- Using compile-time adapters (generics, templates) to eliminate virtual dispatch
- Profiling before optimizing - the overhead is usually negligible
Bidirectional Adaptation
When two systems must call each other through adapters, you risk circular dependencies. Mitigations:
- Implement a two-way adapter that satisfies both interfaces
- Use an event-driven architecture to decouple the communication direction
- Introduce a shared intermediate representation (canonical model)
Partial Adaptation
If the Adaptee cannot fully satisfy the Target interface (missing capabilities), the adapter must decide:
- Throw
UnsupportedOperationExceptionfor unimplemented methods - Provide sensible defaults or no-op implementations
- Document the limitation clearly in the adapter's contract
Thread Safety
If the Adaptee is not thread-safe but the Target interface is expected to be used concurrently, the adapter must add synchronization - blurring the line between Adapter and Decorator.
Interview Follow-ups
Q1: Can you use the Adapter pattern with dependency injection frameworks?
A: Yes - and it's a natural fit. You register the Adapter as the implementation of the Target interface in the DI container. The client receives the Target via constructor injection, completely unaware of the adapter or adaptee. This is how JDBC drivers, logging facades, and payment adapters are typically wired in Spring, Guice, or .NET DI containers.
Q2: How does the Adapter pattern differ from the Strategy pattern?
A: Both involve programming to an interface, but their intent differs. Strategy lets you swap algorithms at runtime - the implementations are interchangeable by design. Adapter makes an existing incompatible class conform to an expected interface - the adaptee was never designed to be interchangeable. Strategy is about behavioral flexibility; Adapter is about structural compatibility.
Q3: How would you handle versioning when the Adaptee's API changes?
A: Isolate the version-specific logic within the adapter. You can maintain multiple adapter implementations (one per API version) behind the same Target interface, selecting the correct one via a factory based on configuration or runtime detection. This keeps the client stable across Adaptee upgrades.
Q4 (Hint Only): How would you design an adapter that needs to aggregate data from multiple legacy systems into a single response?
Hint: Think about composing multiple adaptees within one adapter and consider how you'd handle partial failures (circuit breaker, fallback defaults) and data merging strategies.
Q5 (Hint Only): What happens if the Target interface evolves and adds new methods - how do you handle backward compatibility in existing adapters?
Hint: Consider default methods (Java 8+), abstract base classes with default implementations, and the implications of the Interface Segregation Principle on adapter design.
Counter Questions to Ask Interviewer
-
"Are we adapting a single class or an entire subsystem?" - Determines whether Adapter or Facade is more appropriate.
-
"Do we control the source code of the class being adapted?" - If yes, refactoring the original interface might be simpler than introducing an adapter.
-
"Is this a temporary bridge during migration or a permanent architectural boundary?" - Influences whether to invest in a robust adapter or a lightweight shim.
-
"What's the expected call frequency through the adapter?" - Helps assess whether the indirection overhead matters.
-
"Are there multiple implementations we need to adapt to the same interface?" - Signals whether to combine Adapter with Abstract Factory for a family of adapters.
-
"Does the adapted interface need to support bidirectional communication?" - Determines complexity and whether a two-way adapter or event-based approach is needed.
References & Whitepapers
-
Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (1994), Chapter 4: Structural Patterns - Adapter. The canonical reference.
-
Freeman, E., Robson, E. - Head First Design Patterns (2004), Chapter 7: The Adapter and Facade Patterns. Accessible introduction with real-world analogies.
-
Martin, R.C. - Agile Software Development: Principles, Patterns, and Practices (2002). Discusses Adapter in the context of the Dependency Inversion Principle.
-
Java JDBC Architecture - Oracle JDBC Documentation - Real-world adapter pattern at scale.
-
SLF4J Project - slf4j.org - Logging facade demonstrating adapter/bridge architecture.
-
Spring Framework -
HandlerAdapterin Spring MVC adapts various handler types (annotated controllers, simple controllers, HTTP request handlers) to a uniform dispatch interface. -
.NET Framework -
DataAdapterclasses (SqlDataAdapter,OleDbDataAdapter) adapt database-specific protocols to the unifiedDataSetinterface.
Related Topics
- Facade Pattern - Simplifies a subsystem vs. translating an interface
- Decorator Pattern - Adds behavior while keeping the same interface
- Bridge Pattern - Separates abstraction from implementation (designed upfront vs. adapter's retrofit)
- Proxy Pattern - Same interface with controlled access vs. different interface translation
- Composite Pattern - Can be combined with Adapter when adapting tree structures
- Strategy Pattern - Interchangeable algorithms vs. interface translation
- Abstract Factory - Often used to create families of related adapters
- Dependency Inversion Principle - The theoretical foundation that makes adapters valuable
The Adapter pattern is one of the most frequently used structural patterns in enterprise software. Its simplicity belies its power - by introducing a thin translation layer, you gain the freedom to evolve, replace, and test components independently. Master it, and you'll find it appearing naturally in every integration boundary you design.