Adapter Design Pattern Tutorial from Scratch (2026)
The Adapter pattern allows incompatible interfaces to work together by converting the interface of a class into another interface that a client expects. It acts as a wrapper that translates calls from the client format to the format understood by the adaptee, enabling integration of legacy or third-party code without modification.
This tutorial covers both the class adapter (using inheritance) and object adapter (using composition) approaches in Java and C++. You will learn how to make existing classes work with new interfaces, handle API version mismatches, and integrate third-party libraries cleanly.
Object Adapter Using Composition
The object adapter uses composition to wrap the adaptee and implement the target interface. This approach is more flexible than class adapter because it works with any subclass of the adaptee and does not require inheriting from the adaptee class. It is the preferred approach in most cases.
// Target interface that client expects
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee (existing/incompatible class)
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);
}
}
// Object Adapter
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(AdvancedMediaPlayer advancedPlayer) {
this.advancedPlayer = advancedPlayer;
}
public void play(String audioType, String fileName) {
switch (audioType.toLowerCase()) {
case "vlc" -> advancedPlayer.playVlc(fileName);
case "mp4" -> advancedPlayer.playMp4(fileName);
default -> throw new IllegalArgumentException("Unsupported format: " + audioType);
}
}
}
// Client
class AudioPlayer implements MediaPlayer {
private MediaAdapter adapter;
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
} else {
adapter = new MediaAdapter(new AdvancedMediaPlayer());
adapter.play(audioType, fileName);
}
}
}
Class Adapter Using Inheritance
The class adapter uses multiple inheritance (or single inheritance in Java with interface implementation) to extend both the target interface and the adaptee class. This approach has the advantage of overriding adaptee behavior, but it requires the adaptee to be a class with a public constructor and is less flexible for adapting multiple adaptees.
// C++ class adapter using multiple inheritance
class Target {
public:
virtual void request() const {
std::cout << "Target: default behavior" << std::endl;
}
virtual ~Target() = default;
};
class Adaptee {
public:
void specificRequest() const {
std::cout << "Adaptee: specific request" << std::endl;
}
};
// Class Adapter
class Adapter : public Target, private Adaptee {
public:
void request() const override {
std::cout << "Adapter: translating call" << std::endl;
specificRequest();
}
};
// Usage
void clientCode(const Target& target) {
target.request();
}
int main() {
Adapter adapter;
clientCode(adapter);
return 0;
}
Adapter for API Version Compatibility
Adapters are invaluable when dealing with API version migrations. When a third-party library changes its interface between versions, an adapter can bridge the old and new APIs, allowing incremental migration without breaking existing client code. This is a practical application of the pattern in enterprise systems.
// Legacy API
class LegacyPaymentGateway {
public boolean processPayment(String cardNum, double amount, String currency) {
System.out.println("Legacy: processing " + amount + " " + currency);
return true;
}
}
// New API
class NewPaymentGateway {
public PaymentResult charge(PaymentRequest request) {
System.out.println("New: charging " + request.getAmount());
return new PaymentResult(true, "txn_123");
}
}
// Adapter making new API look like legacy API
class PaymentAdapter extends LegacyPaymentGateway {
private NewPaymentGateway newGateway;
public PaymentAdapter(NewPaymentGateway newGateway) {
this.newGateway = newGateway;
}
@Override
public boolean processPayment(String cardNum, double amount, String currency) {
PaymentRequest request = new PaymentRequest.Builder()
.cardNumber(cardNum)
.amount(amount)
.currency(currency)
.build();
return newGateway.charge(request).isSuccess();
}
}
Frequently Asked Questions
What is the difference between Class Adapter and Object Adapter?
Class Adapter uses inheritance (extends Adaptee, implements Target), while Object Adapter uses composition (holds a reference to Adaptee). Object Adapter is more flexible as it works with any Adaptee subclass and does not expose Adaptee methods. Class Adapter can override Adaptee behavior.
When should I use the Adapter pattern?
Use Adapter when you want to use an existing class with an incompatible interface, when you need to integrate third-party or legacy code into a new system, or when you want to create a reusable class that cooperates with unrelated or unforeseen classes.
How is Adapter different from Facade?
Adapter converts one interface to another (interface translation), while Facade provides a simplified interface to a subsystem (interface simplification). Adapter is used for compatibility, Facade for ease of use.
Does Adapter pattern affect performance?
The performance impact is negligible for most applications. The adapter adds a single method call layer of indirection. For performance-critical code (e.g., high-frequency trading), the overhead may be measurable but is typically acceptable.
Originally published on Ayodhyyya. Last updated June 1, 2026.