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:

  1. 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).

  2. 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.

  3. Interface Mismatch After Refactoring - Two subsystems evolved independently and now need to collaborate, but their method signatures, data formats, or protocols diverge.

  4. Testing & Mocking - Wrapping an external service behind an adapter interface enables substitution with test doubles.

  5. 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 UseWhen NOT to Use
You need to use an existing class but its interface doesn't match what you needThe interfaces are already compatible - adding an adapter is unnecessary indirection
You want to create a reusable class that cooperates with unrelated or unforeseen classesYou need to add new behavior or responsibilities (use Decorator instead)
You need to integrate a third-party library without coupling your code to itYou want to simplify a complex subsystem (use Facade instead)
You're migrating from one implementation to another and need a transitional layerThe adaptee is unstable and changes frequently - the adapter becomes a maintenance burden
You want to isolate legacy code behind a clean interface for testabilityYou 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

ParticipantRole
TargetThe interface the client expects
AdapterTranslates calls from Target interface to Adaptee interface
AdapteeThe existing class with an incompatible interface
ClientCollaborates 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

CriterionObject Adapter (Composition)Class Adapter (Inheritance)
MechanismHolds reference to adaptee instanceInherits from both Target and Adaptee
Language SupportAll OOP languagesOnly languages with multiple inheritance (C++, Python)
FlexibilityCan adapt any subclass of Adaptee at runtimeBound to one specific Adaptee class at compile time
Override BehaviorCannot override Adaptee methods directlyCan override Adaptee methods (more control)
CouplingLoose - depends on Adaptee's public interfaceTight - inherits all of Adaptee's internals
Multiple AdapteesCan compose multiple adaptees in one adapterRequires multiple inheritance from all adaptees
SOLID ComplianceFavors Single Responsibility and Open/ClosedMay violate SRP by mixing two class hierarchies
TestabilityEasy to mock the adapteeHarder to isolate in tests
RecommendationPreferred in most scenariosUse 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

AspectAdapterFacadeDecorator
IntentMake incompatible interfaces compatibleSimplify a complex subsystemAdd responsibilities dynamically
Interface ChangeConverts one interface to anotherDefines a new simplified interfaceKeeps the same interface
Number of Wrapped ObjectsUsually oneMultiple subsystem objectsOne
Client AwarenessClient uses Target interface, unaware of AdapteeClient uses Facade, unaware of subsystem complexityClient uses same interface, unaware of added behavior
Structural ChangeChanges interface shapeReduces interface surface areaPreserves interface, adds layers
Typical Use CaseLegacy integration, third-party wrappingProviding a simple API over a libraryAdding logging, caching, validation transparently
Composition DepthSingle levelSingle level over many objectsCan be nested (stacked decorators)

Advantages & Disadvantages

AdvantagesDisadvantages
Single Responsibility - separates interface conversion from business logicIncreased complexity - adds extra classes and indirection
Open/Closed Principle - introduce new adapters without modifying existing codePerformance overhead - extra delegation layer (usually negligible)
Decoupling - client is isolated from third-party/legacy changesDebugging difficulty - call stack is deeper; harder to trace through adapters
Testability - easy to mock the Target interface in testsOver-engineering risk - unnecessary when interfaces are already compatible
Reusability - same adapter works for any client expecting the TargetData loss risk - if Adaptee's interface is richer, adapter may discard information
Gradual migration - enables incremental replacement of legacy systemsMaintenance 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 UnsupportedOperationException for 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

  1. "Are we adapting a single class or an entire subsystem?" - Determines whether Adapter or Facade is more appropriate.

  2. "Do we control the source code of the class being adapted?" - If yes, refactoring the original interface might be simpler than introducing an adapter.

  3. "Is this a temporary bridge during migration or a permanent architectural boundary?" - Influences whether to invest in a robust adapter or a lightweight shim.

  4. "What's the expected call frequency through the adapter?" - Helps assess whether the indirection overhead matters.

  5. "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.

  6. "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

  1. 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.

  2. Freeman, E., Robson, E. - Head First Design Patterns (2004), Chapter 7: The Adapter and Facade Patterns. Accessible introduction with real-world analogies.

  3. Martin, R.C. - Agile Software Development: Principles, Patterns, and Practices (2002). Discusses Adapter in the context of the Dependency Inversion Principle.

  4. Java JDBC Architecture - Oracle JDBC Documentation - Real-world adapter pattern at scale.

  5. SLF4J Project - slf4j.org - Logging facade demonstrating adapter/bridge architecture.

  6. Spring Framework - HandlerAdapter in Spring MVC adapts various handler types (annotated controllers, simple controllers, HTTP request handlers) to a uniform dispatch interface.

  7. .NET Framework - DataAdapter classes (SqlDataAdapter, OleDbDataAdapter) adapt database-specific protocols to the unified DataSet interface.



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.