June 17, 2026 in patterns5 minutes
A intermediate-level guide to Adapter: before-and-after java code and diagrams for a CS student.
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
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:
DataProcessor so the Client is happy.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]
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.
It is easy to confuse the Adapter with other structural patterns. Note these distinctions:
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.
Using the Adapter to bridge the interface gap
DataProcessor adapter = new LegacyServiceAdapter(new LegacyService());
Client client = new Client(adapter);
client.run();