Adapter Design Pattern
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Real-World Analogy
Think of a Power Adapter
When you travel from the US to Europe, your laptop charger plug won't fit European wall sockets. You use a power adapter — it doesn't change what your charger does, it simply makes the incompatible plug fit into the socket. The Adapter pattern does the same for software interfaces.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
A["🔌 US Plug"] -->|"doesn't fit"| B["🔄 Adapter"]
B -->|"converts"| C["🏠 EU Socket"]
C -->|"powers"| D["⚡ Success!"]
style A fill:#E3F2FD,stroke:#1565C0,color:#000
style B fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000
style C fill:#E8F5E9,stroke:#2E7D32,color:#000
style D fill:#FFF8E1,stroke:#F9A825,color:#000
Pattern Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Client["🖥️ Client"] -->|"uses"| Target[["🎯 Target Interface"]]
Adapter{{"🔄 Adapter"}} -->|"implements"| Target
Adapter -->|"delegates"| Adaptee(["📦 Adaptee"])
style Client fill:#E8F5E9,stroke:#2E7D32,color:#000
style Target fill:#FFF3E0,stroke:#E65100,color:#000
style Adapter fill:#E3F2FD,stroke:#1565C0,color:#000
style Adaptee fill:#FFF8E1,stroke:#F9A825,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class MediaPlayer {
<<interface>>
+play(audioType, fileName) void
}
class AdvancedMediaPlayer {
+playVlc(fileName) void
+playMp4(fileName) void
}
class MediaAdapter {
-advancedPlayer: AdvancedMediaPlayer
+play(audioType, fileName) void
}
class AudioPlayer {
-mediaAdapter: MediaAdapter
+play(audioType, fileName) void
}
MediaAdapter ..|> MediaPlayer : implements
AudioPlayer ..|> MediaPlayer : implements
MediaAdapter *-- AdvancedMediaPlayer : wraps
AudioPlayer --> MediaAdapter : delegates Object Adapter vs Class Adapter
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
subgraph OA["Object Adapter (Composition)"]
direction LR
OA_Adapter{{"🔄 Adapter"}} -->|"has-a"| OA_Adaptee(["📦 Adaptee"])
OA_Adapter -->|"implements"| OA_Target[["🎯 Target"]]
end
subgraph CA["Class Adapter (Inheritance)"]
direction LR
CA_Adapter{{"🔄 Adapter"}} -->|"extends"| CA_Adaptee(["📦 Adaptee"])
CA_Adapter -->|"implements"| CA_Target[["🎯 Target"]]
end
style OA_Adapter fill:#E3F2FD,stroke:#1565C0,color:#000
style OA_Adaptee fill:#FFF8E1,stroke:#F9A825,color:#000
style OA_Target fill:#FFF3E0,stroke:#E65100,color:#000
style CA_Adapter fill:#E3F2FD,stroke:#1565C0,color:#000
style CA_Adaptee fill:#FFF8E1,stroke:#F9A825,color:#000
style CA_Target fill:#FFF3E0,stroke:#E65100,color:#000
The Problem
You're integrating a third-party analytics library that returns data in XML format. But your application's reporting module exclusively works with JSON. You can't modify the third-party library, and rewriting your reporting module would break existing code.
Without an adapter, you'd face:
- Tight coupling to specific data formats
- Inability to swap implementations
- Violation of the Open/Closed Principle
Without This Pattern
// BAD: Client directly coupled to third-party XML library
public class ReportGenerator {
public void generateReport() {
LegacyXmlService xmlService = new LegacyXmlService();
String xmlData = xmlService.getXmlData();
// Parsing XML manually in every client that needs JSON
String json = xmlData.replace("<data><name>", "{\"name\": \"")
.replace("</name></data>", "\"}");
// What if we switch to a YAML library tomorrow?
// Every single client class must be rewritten!
renderChart(json);
}
// Another method also needs the conversion — duplicated logic
public void exportData() {
LegacyXmlService xmlService = new LegacyXmlService();
String xmlData = xmlService.getXmlData();
String json = xmlData.replace("<data><name>", "{\"name\": \"")
.replace("</name></data>", "\"}");
writeToFile(json);
}
}
Problems:
- Tight coupling: Every client directly depends on the third-party
LegacyXmlServiceand its XML format — changing the library forces changes everywhere - Duplicated conversion logic: XML-to-JSON translation is copy-pasted across multiple methods and classes
- Violates Open/Closed Principle: Adding a new data source (e.g., CSV) requires modifying all existing client classes
- Violates Single Responsibility Principle: The
ReportGeneratorhandles both report logic AND data format conversion - Pain point: When the third-party library updates its API or you need to swap it for a different vendor, you must hunt down and rewrite every place that touches it
The Solution
The Adapter pattern introduces a wrapper class that translates one interface into another. The client works with the Target interface; the Adapter translates those calls into the format the Adaptee understands.
Two flavors:
| Type | Mechanism | Java Support |
|---|---|---|
| Object Adapter | Composition (has-a) | Preferred in Java |
| Class Adapter | Multiple Inheritance (is-a) | Not directly possible (no MI) — use interfaces |
Implementation
// Target interface - what our client expects
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee - incompatible interface (third-party or legacy)
public class AdvancedMediaPlayer {
public void playVlc(String fileName) {
System.out.println("Playing VLC file: " + fileName);
}
public void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
}
// Adapter - bridges the gap using COMPOSITION
public class MediaAdapter implements MediaPlayer {
private final AdvancedMediaPlayer advancedPlayer;
public MediaAdapter() {
this.advancedPlayer = new AdvancedMediaPlayer();
}
@Override
public void play(String audioType, String fileName) {
switch (audioType.toLowerCase()) {
case "vlc" -> advancedPlayer.playVlc(fileName);
case "mp4" -> advancedPlayer.playMp4(fileName);
default -> throw new UnsupportedOperationException(
"Format not supported: " + audioType);
}
}
}
// Client code
public class AudioPlayer implements MediaPlayer {
private final MediaAdapter mediaAdapter = new MediaAdapter();
@Override
public void play(String audioType, String fileName) {
if ("mp3".equalsIgnoreCase(audioType)) {
System.out.println("Playing MP3 file: " + fileName);
} else {
mediaAdapter.play(audioType, fileName);
}
}
}
// In Java, we simulate class adapter using interface + extending adaptee
public interface Target {
String getData();
}
// Adaptee with incompatible interface
public class LegacyXmlService {
public String getXmlData() {
return "<data><name>Adapter Pattern</name></data>";
}
}
// Class Adapter - extends Adaptee AND implements Target
public class XmlToJsonAdapter extends LegacyXmlService implements Target {
@Override
public String getData() {
String xml = getXmlData(); // inherited from LegacyXmlService
return convertXmlToJson(xml);
}
private String convertXmlToJson(String xml) {
// Conversion logic (simplified)
return "{\"name\": \"Adapter Pattern\"}";
}
}
// Client only knows Target interface
public class ReportGenerator {
private final Target dataSource;
public ReportGenerator(Target dataSource) {
this.dataSource = dataSource;
}
public void generateReport() {
String json = dataSource.getData();
System.out.println("Report data: " + json);
}
}
// Target interface
public interface UserRepository {
User findById(Long id);
List<User> findAll();
void save(User user);
}
// Adaptee — Legacy JDBC-based DAO
public class LegacyUserDao {
public Map<String, Object> queryUserById(long id) { /* ... */ }
public List<Map<String, Object>> queryAllUsers() { /* ... */ }
public void insertUser(Map<String, Object> userData) { /* ... */ }
}
// Adapter
@Repository
public class UserRepositoryAdapter implements UserRepository {
private final LegacyUserDao legacyDao;
public UserRepositoryAdapter(LegacyUserDao legacyDao) {
this.legacyDao = legacyDao;
}
@Override
public User findById(Long id) {
Map<String, Object> raw = legacyDao.queryUserById(id);
return mapToUser(raw);
}
@Override
public List<User> findAll() {
return legacyDao.queryAllUsers().stream()
.map(this::mapToUser)
.collect(Collectors.toList());
}
@Override
public void save(User user) {
legacyDao.insertUser(mapToRaw(user));
}
private User mapToUser(Map<String, Object> raw) {
return new User((Long) raw.get("id"), (String) raw.get("name"));
}
private Map<String, Object> mapToRaw(User user) {
return Map.of("id", user.getId(), "name", user.getName());
}
}
When to Use
- You want to use an existing class but its interface doesn't match what you need
- You're building a reusable class that must cooperate with unrelated/unforeseen classes
- You need to integrate legacy code with a modern system without modifying the legacy code
- You're wrapping third-party libraries to isolate your code from external changes
- You need to provide a unified interface for multiple classes with different interfaces
Real-World Examples
| Where | Example |
|---|---|
| JDK | Arrays.asList() — adapts array to List interface |
| JDK | InputStreamReader — adapts InputStream to Reader |
| JDK | Collections.enumeration() — adapts Collection to Enumeration |
| Spring | HandlerAdapter — adapts various handler types to a common interface |
| Spring | JpaVendorAdapter — adapts different JPA providers |
| SLF4J | Entire library is an adapter over logging frameworks |
| Jackson | ObjectMapper adapts Java objects to/from JSON |
Pitfalls
Common Mistakes
- Over-adapting: Creating adapters for classes that can easily be refactored — increases unnecessary indirection
- Two-way adapters: Trying to adapt both directions makes the adapter overly complex
- Using Class Adapter in Java: Java doesn't support multiple inheritance of classes — prefer Object Adapter (composition)
- Leaking Adaptee details: The adapter should completely hide the Adaptee's interface from the client
- Not following ISP: Creating a "fat" target interface forces the adapter to implement methods it doesn't need
Key Takeaways
Summary
| Aspect | Detail |
|---|---|
| Intent | Make incompatible interfaces work together |
| Mechanism | Wraps an existing class with a new interface |
| Preferred Type | Object Adapter (composition over inheritance) |
| Key Principle | Open/Closed Principle — extend without modifying |
| Interview Tip | Adapter converts an interface; Decorator adds behavior; Facade simplifies a subsystem |
| One-liner | "If you have a square peg and a round hole, you need an Adapter" |