Adapter

June 17, 2026 in patterns 5 minutes

A intermediate-level guide to Adapter: before-and-after java code and diagrams for a CS student.

The Mismatch Problem

Imagine you have built a sophisticated smart home system. Your software is designed to talk to lightbulbs using a specific standard: you call a method named turnOn(). Everything works perfectly until you buy a high-end, designer lamp from Europe. This lamp is excellent, but its internal software only understands one command: executeTask(String powerState).

You cannot rewrite the lamp’s firmware, and you certainly do not want to rewrite your entire smart home application just to accommodate one new device. Currently, your application speaks “SmartHome Language,” but the service speaks “Legacy/Foreign Language.” They are incompatible because their method signatures—the names of the methods and the parameters they accept—do not match.

// Application expects this interface
interface DataProcessor {
    void processData(String data);
}

// Third-party library with a different signature
class LegacyService {
    public void executeTask(String input) {
        System.out.println("Executing legacy task: " + input);
    }
}

// Problem: The client is tightly coupled to the incompatible third-party method.
// To use LegacyService, we would have to change our existing DataProcessor implementations,
// which violates the Open/Closed Principle if we can't modify the legacy library.
class Client {
    private LegacyService service = new LegacyService();

    public void run() {
        // Error: Cannot call processData on LegacyService,
        // and cannot change LegacyService to add processData().
        service.executeTask("some data"); 
    }
}

The diagram below illustrates this structural disconnect. The Client is looking for a DataProcessor, but it has been handed a LegacyService. Because LegacyService does not implement DataProcessor, they cannot work together directly.

  classDiagram
    class DataProcessor {
        <<interface>>
        +processData(String data) void
    }
    class Client {
        -LegacyService service
        +run() void
    }
    class LegacyService {
        +executeTask(String input) void
    }
    Client ..> LegacyService : uses incompatible

Defining the Adapter

To bridge this gap, we use an Adapter. In software engineering, an Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.

Think of it as the physical plug adapter you use when traveling: it sits between the wall outlet and your device. The adapter implements the “shape” required by the wall but holds the internal logic needed to connect to your device’s specific pins.

In our Java example, we create a new class—the LegacyServiceAdapter. This class performs two critical roles:

  1. It implements the target interface: It pretends to be a DataProcessor so the Client is happy.
  2. It wraps the service: It holds a private reference to the LegacyService.

When the Client calls processData(), the Adapter intercepts that call and translates it into a call to executeTask() on the legacy object.

// Application expects this interface
interface DataProcessor {
    void processData(String data);
}

// Third-party library (remains unchanged)
class LegacyService {
    public void executeTask(String input) {
        System.out.println("Executing legacy task: " + input);
    }
}

// The Adapter makes the incompatible LegacyService compatible with DataProcessor
class LegacyServiceAdapter implements DataProcessor {
    private final LegacyService legacyService;

    public LegacyServiceAdapter(LegacyService legacyService) {
        this.legacyService = legacyService;
    }

    @Override
    public void processData(String data) {
        // Translates the call from our interface to the specific method of the service
        legacyService.executeTask(data);
    }
}

// Client now works with any DataProcessor, including the Adapter
class Client {
    private final DataProcessor processor;

    public Client(DataProcessor processor) {
        this.processor = processor;
    }

    public void run() {
        processor.processData("some data");
    }
}

With the adapter in place, the relationship changes from direct incompatibility to a structured delegation. The LegacyServiceAdapter acts as the mediator.

  classDiagram
    class DataProcessor {
        <<interface>>
        +processData(String data) void
    }
    class LegacyServiceAdapter {
        -LegacyService legacyService
        +processData(String data) void
    }
    class LegacyService {
        +executeTask(String input) void
    }
    LegacyServiceAdapter ..|> DataProcessor : implements
    LegacyServiceAdapter o-- LegacyService : wraps

To understand what happens at runtime, follow the sequence of a single method call. The Client believes it is simply talking to a standard processor, unaware that a translation is occurring behind the scenes.

  sequenceDiagram
    participant C as Client
    participant A as LegacyServiceAdapter
    participant L as LegacyService

    C->>A: processData(data)
    A->>L: executeTask(data)
    L-->>A: [void]
    A-->>C: [void]

How It Works: Interface/Implementation Separation

The magic of the Adapter pattern relies on Interface/Implementation Separation. By using an interface (DataProcessor), the Client is decoupled from the specific details of how data is processed. The Client only cares that a method named processData exists.

By inserting the Adapter between the two, we satisfy the Open/Closed Principle: we have extended the system’s capabilities (we can now use the legacy service) without modifying the existing, working code of the Client or the LegacyService.

Distinguishing Patterns

It is easy to confuse the Adapter with other structural patterns. Note these distinctions:

  • Adapter vs. Decorator: A Decorator adds new responsibilities to an object (like adding “Logging” functionality to a processor). An Adapter’s only job is to change the interface so two things can talk.
  • Adapter vs. Proxy: A Proxy provides the exact same interface as the object it wraps but controls access to it (for security or lazy loading). An Adapter intentionally changes the interface.

When to Use It, and When Not To

The Adapter pattern is a lifesaver when integrating third-party libraries, legacy codebases, or any component where you do not own the source code and cannot change its method signatures.

However, do not use an Adapter as a way to “fix” messy code within your own system. If you find yourself writing adapters for every class in your application, you are likely suffering from poor design rather than interface incompatibility. Using too many adapters can lead to increased complexity and extra layers of indirection that make the code harder to follow.

Takeaways

  • Use an Adapter when two existing interfaces must work together but cannot be modified.
  • The Adapter implements the target interface and wraps the incompatible service via association or aggregation.
  • It promotes decoupling by isolating third-party dependencies behind a stable, internal interface.

Usage

Using the Adapter to bridge the interface gap

DataProcessor adapter = new LegacyServiceAdapter(new LegacyService());
Client client = new Client(adapter);
client.run();