You’re integrating a new payment gateway. Your code expects a PaymentProcessor interface. The vendor’s SDK has a completely different API structure. You can’t modify the SDK, and you don’t want to rewrite your code.

This is where Adapter shines.

What is the Adapter Pattern?

Adapter converts the interface of a class into another interface that clients expect. It lets classes work together that couldn’t otherwise because of incompatible interfaces.

Think of a power adapter. Your laptop has a US plug. The outlet is European. The adapter sits in between, converting one interface to another.

Adapter Design Pattern class diagram showing Target interface, Adapter class that wraps Legacy class, converting incompatible interfaces

The Client works with the Target interface. The Adapter implements Target and wraps the Legacy class, translating calls.

When to Use Adapter

Use Adapter When Skip Adapter When
Integrating third-party libraries with different APIs You control both interfaces and can modify them
Working with legacy systems you can’t modify A simple method call change would suffice
Creating reusable components for various interfaces The interface mismatch is trivial
You want to isolate your code from external changes Adding indirection isn’t worth the benefit

Adapter is reactive. You use it when interfaces don’t match. Bridge is proactive, you use it when designing upfront.

Implementation

Let’s integrate multiple analytics providers, each with a different API.

The Target Interface (What Your Code Expects)

1
2
3
4
5
public interface AnalyticsService {
    void trackEvent(String eventName, Map<String, Object> properties);
    void identifyUser(String userId, Map<String, Object> traits);
    void trackPageView(String pageName);
}

Third-Party Libraries (What You Actually Have)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Google Analytics has a completely different API
public class GoogleAnalytics {
    public void send(HitBuilder hit) {
        // Google's implementation
    }
    
    public void setUserId(String id) {
        // ...
    }
}

// Mixpanel has its own approach
public class MixpanelClient {
    public void track(String event, JSONObject props) {
        // Mixpanel's implementation
    }
    
    public void identify(String distinctId) {
        // ...
    }
    
    public void peopleSet(JSONObject properties) {
        // ...
    }
}

// Amplitude is different again
public class AmplitudeClient {
    public void logEvent(String eventType, JSONObject eventProperties) {
        // Amplitude's implementation
    }
    
    public void setUserId(String userId) {
        // ...
    }
    
    public void setUserProperties(JSONObject properties) {
        // ...
    }
}

The Adapters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class GoogleAnalyticsAdapter implements AnalyticsService {
    private final GoogleAnalytics ga;
    
    public GoogleAnalyticsAdapter(GoogleAnalytics ga) {
        this.ga = ga;
    }
    
    @Override
    public void trackEvent(String eventName, Map<String, Object> properties) {
        HitBuilder hit = new HitBuilder.EventBuilder()
            .setCategory(properties.getOrDefault("category", "default").toString())
            .setAction(eventName)
            .setLabel(properties.getOrDefault("label", "").toString());
        
        if (properties.containsKey("value")) {
            hit.setValue(((Number) properties.get("value")).longValue());
        }
        
        ga.send(hit.build());
    }
    
    @Override
    public void identifyUser(String userId, Map<String, Object> traits) {
        ga.setUserId(userId);
        // GA doesn't support traits the same way, but we can send as custom dimensions
        for (Map.Entry<String, Object> trait : traits.entrySet()) {
            ga.setCustomDimension(trait.getKey(), trait.getValue().toString());
        }
    }
    
    @Override
    public void trackPageView(String pageName) {
        HitBuilder hit = new HitBuilder.ScreenViewBuilder()
            .setScreenName(pageName)
            .build();
        ga.send(hit);
    }
}

public class MixpanelAdapter implements AnalyticsService {
    private final MixpanelClient mixpanel;
    
    public MixpanelAdapter(MixpanelClient mixpanel) {
        this.mixpanel = mixpanel;
    }
    
    @Override
    public void trackEvent(String eventName, Map<String, Object> properties) {
        JSONObject props = new JSONObject(properties);
        mixpanel.track(eventName, props);
    }
    
    @Override
    public void identifyUser(String userId, Map<String, Object> traits) {
        mixpanel.identify(userId);
        mixpanel.peopleSet(new JSONObject(traits));
    }
    
    @Override
    public void trackPageView(String pageName) {
        JSONObject props = new JSONObject();
        props.put("page", pageName);
        mixpanel.track("Page View", props);
    }
}

public class AmplitudeAdapter implements AnalyticsService {
    private final AmplitudeClient amplitude;
    
    public AmplitudeAdapter(AmplitudeClient amplitude) {
        this.amplitude = amplitude;
    }
    
    @Override
    public void trackEvent(String eventName, Map<String, Object> properties) {
        JSONObject props = new JSONObject(properties);
        amplitude.logEvent(eventName, props);
    }
    
    @Override
    public void identifyUser(String userId, Map<String, Object> traits) {
        amplitude.setUserId(userId);
        amplitude.setUserProperties(new JSONObject(traits));
    }
    
    @Override
    public void trackPageView(String pageName) {
        JSONObject props = new JSONObject();
        props.put("page_name", pageName);
        amplitude.logEvent("Page Viewed", props);
    }
}

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class UserService {
    private final AnalyticsService analytics;
    
    public UserService(AnalyticsService analytics) {
        this.analytics = analytics;
    }
    
    public void registerUser(User user) {
        // Business logic
        userRepository.save(user);
        
        // Analytics - works with any provider
        analytics.identifyUser(user.getId(), Map.of(
            "name", user.getName(),
            "email", user.getEmail(),
            "plan", user.getPlan()
        ));
        
        analytics.trackEvent("User Registered", Map.of(
            "source", user.getRegistrationSource(),
            "plan", user.getPlan()
        ));
    }
}

// Configuration - swap providers without changing UserService
AnalyticsService analytics = new MixpanelAdapter(new MixpanelClient("api-key"));
// or
AnalyticsService analytics = new AmplitudeAdapter(new AmplitudeClient("api-key"));
// or
AnalyticsService analytics = new GoogleAnalyticsAdapter(new GoogleAnalytics("tracking-id"));

UserService userService = new UserService(analytics);

How It Works

sequenceDiagram
    participant Client as UserService
    participant Adapter as MixpanelAdapter
    participant Adaptee as MixpanelClient
    
    Client->>Adapter: trackEvent("User Registered", props)
    Note over Adapter: Convert Map to JSONObject
    Adapter->>Adaptee: track("User Registered", jsonProps)
    Adaptee-->>Adapter: void
    Adapter-->>Client: void
    
    Client->>Adapter: identifyUser("user-123", traits)
    Adapter->>Adaptee: identify("user-123")
    Adapter->>Adaptee: peopleSet(jsonTraits)
    Adaptee-->>Adapter: void
    Adapter-->>Client: void

The client calls the Target interface. The Adapter translates these calls to the Adaptee’s specific API.

Object Adapter vs Class Adapter

There are two variations:

Object Adapter (Composition)

The example above uses object adapter. The adapter holds a reference to the adaptee and delegates calls.

1
2
3
4
5
6
7
8
9
10
11
12
public class MixpanelAdapter implements AnalyticsService {
    private final MixpanelClient mixpanel;  // Composition
    
    public MixpanelAdapter(MixpanelClient mixpanel) {
        this.mixpanel = mixpanel;
    }
    
    @Override
    public void trackEvent(String eventName, Map<String, Object> properties) {
        mixpanel.track(eventName, new JSONObject(properties));
    }
}

Class Adapter (Inheritance)

Class adapter extends the adaptee and implements the target. Less common in Java because it requires multiple inheritance.

1
2
3
4
5
6
7
8
public class MixpanelAdapter extends MixpanelClient implements AnalyticsService {
    
    @Override
    public void trackEvent(String eventName, Map<String, Object> properties) {
        // Can call inherited methods directly
        this.track(eventName, new JSONObject(properties));
    }
}

Object adapter is more flexible. It can adapt multiple adaptees and works with any subclass of the adaptee.

Two-Way Adapter

Sometimes you need translation in both directions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public interface ModernPaymentGateway {
    PaymentResult charge(PaymentRequest request);
    RefundResult refund(String transactionId, Money amount);
}

public interface LegacyPaymentSystem {
    int processPayment(String cardNumber, double amount, String currency);
    boolean reverseTransaction(int transactionId);
}

public class TwoWayPaymentAdapter implements ModernPaymentGateway, LegacyPaymentSystem {
    private final LegacyPaymentProcessor legacyProcessor;
    private final Map<String, Integer> transactionMapping = new ConcurrentHashMap<>();
    
    public TwoWayPaymentAdapter(LegacyPaymentProcessor legacyProcessor) {
        this.legacyProcessor = legacyProcessor;
    }
    
    // Modern interface calls
    @Override
    public PaymentResult charge(PaymentRequest request) {
        int legacyTxId = legacyProcessor.processPayment(
            request.getCardNumber(),
            request.getAmount().doubleValue(),
            request.getCurrency().getCode()
        );
        
        String modernTxId = UUID.randomUUID().toString();
        transactionMapping.put(modernTxId, legacyTxId);
        
        return new PaymentResult(modernTxId, PaymentStatus.SUCCESS);
    }
    
    @Override
    public RefundResult refund(String transactionId, Money amount) {
        Integer legacyId = transactionMapping.get(transactionId);
        boolean success = legacyProcessor.reverseTransaction(legacyId);
        return new RefundResult(success ? RefundStatus.COMPLETED : RefundStatus.FAILED);
    }
    
    // Legacy interface calls (for old code still using legacy interface)
    @Override
    public int processPayment(String cardNumber, double amount, String currency) {
        return legacyProcessor.processPayment(cardNumber, amount, currency);
    }
    
    @Override
    public boolean reverseTransaction(int transactionId) {
        return legacyProcessor.reverseTransaction(transactionId);
    }
}

Common Mistakes

1. Adapter Doing Too Much

An adapter should translate, not add business logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Wrong - adapter has business logic
public class PaymentAdapter implements PaymentService {
    @Override
    public Result pay(Amount amount) {
        if (amount.getValue() > 10000) {
            requireApproval();  // This is business logic, not adaptation
        }
        return gateway.charge(convertAmount(amount));
    }
}

// Right - adapter only translates
public class PaymentAdapter implements PaymentService {
    @Override
    public Result pay(Amount amount) {
        return convertResult(gateway.charge(convertAmount(amount)));
    }
}

2. Not Handling All Interface Methods

If the adaptee doesn’t support something the target requires:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LimitedStorageAdapter implements CloudStorage {
    private final LocalFileSystem fs;
    
    @Override
    public void uploadFile(String path, byte[] content) {
        fs.write(path, content);
    }
    
    @Override
    public void setAccessPolicy(String path, AccessPolicy policy) {
        // Local filesystem doesn't have access policies
        throw new UnsupportedOperationException("Access policies not supported");
        // Or log a warning and no-op
        // Or provide a reasonable default behavior
    }
}

Be explicit about limitations.

3. Leaking Adaptee Details

The adapter should hide the adaptee completely:

1
2
3
4
5
6
7
8
9
10
11
12
// Wrong - exposes adaptee
public class StorageAdapter implements CloudStorage {
    public AmazonS3 getS3Client() {  // Leaks implementation
        return this.s3Client;
    }
}

// Right - adapter is opaque
public class StorageAdapter implements CloudStorage {
    private final AmazonS3 s3Client;  // Private, not exposed
    // Only implements CloudStorage methods
}

Real-World Examples

JDBC: Each database vendor provides a JDBC driver that adapts their native protocol to the JDBC interface.

SLF4J: Adapts various logging frameworks (Log4j, Logback, java.util.logging) to a common interface.

Spring MVC: HandlerAdapter adapts different handler types to the DispatcherServlet.

Collections: Arrays.asList() adapts an array to the List interface.

Bridge separates abstraction from implementation by design. Adapter makes incompatible things work together after the fact.

Decorator keeps the same interface but adds behavior. Adapter changes the interface.

Proxy provides the same interface but controls access. Adapter provides a different interface.

Facade defines a new, simpler interface for a complex subsystem. Adapter wraps a single object to make it compatible.

Wrapping Up

Adapter lets you integrate incompatible interfaces without modifying existing code. It wraps an object with a different interface inside an adapter that implements what your code expects.

Use Adapter for third-party library integration, legacy system connectivity, and isolating your code from external API changes. Prefer object adapter over class adapter for flexibility.

Keep adapters thin. They should translate, not add logic. If you’re adding significant behavior, you might need a Decorator instead.


Further Reading: