Last Updated: February 22, 2026
The Adapter Design Pattern is a structural design pattern that allows incompatible interfaces to work together by converting the interface of one class into another that the client expects.
It’s particularly useful in situations where:
When faced with incompatible interfaces, developers often resort to rewriting large parts of code or embedding conditionals like if (legacyType) to handle special cases. But as more incompatible services or modules are introduced, this approach quickly becomes messy, tightly coupled, and violates the Open/Closed Principle making the system hard to scale or refactor.
The Adapter Pattern solves this by introducing a wrapper class that sits between your system and the incompatible component. It translates calls from your interface into calls the legacy or third-party system understands without changing either side.
Let’s walk through a real-world example to see how we can apply the Adapter Pattern to seamlessly integrate incompatible components and create a more flexible and maintainable architecture.
Imagine you are building the checkout component of an e-commerce application. Your checkout service is designed to work with a PaymentProcessor interface for handling payments.
Here’s the contract your CheckoutService expects any payment provider to follow:
This abstraction makes it easy to swap payment providers without changing any core business logic.
Your team already has an internal payment processor that fits this interface perfectly:
Your CheckoutService uses this interface and works beautifully with the in-house payment processor:
Here’s how it gets called from your main e-commerce application:
Everything works smoothly. You’ve decoupled your checkout business logic from the underlying payment implementation, allowing future flexibility. Great job so far.
Now management drops a new requirement: integrate with a legacy third-party payment provider. Its SDK is battle-tested and reliable, but its interface looks nothing like yours.
Here’s what that legacy payment class looks like:
You now have two interfaces that do the same thing but speak different languages:
And here is the constraint:
CheckoutService, it is used system-wide and depends on PaymentProcessorLegacyGateway, it is from an external vendorWhat you need is a translator, a class that sits between CheckoutService and LegacyGateway, adapting the incompatible interface into one that works with your system.
This is exactly what the Adapter Design Pattern does.
The Adapter pattern converts the interface of an existing class into a different interface that clients expect. It lets classes work together that otherwise could not because of incompatible interfaces.
Two characteristics define the pattern:
Imagine you're traveling from the United States to Europe. Your laptop charger uses a Type A plug (used in the US), but European wall sockets expect a Type C plug.
You can’t plug your charger in directly, the interfaces don’t match.
Instead of buying a new charger, you use a travel plug adapter. This device accepts your Type A plug and converts it into a Type C shape that fits into the European socket.
The Adapter pattern does the same thing for software interfaces.
There are two primary ways to implement an adapter, depending on the language and use case:
Adapter has four participants.
The interface that the client code depends on. Every method call from the client goes through this interface.
In our payment example, PaymentProcessor is the Target. The checkout service only knows about processPayment(), isPaymentSuccessful(), and getTransactionId().
The existing class with a useful implementation but an incompatible interface.
In our example, LegacyGateway is the Adaptee. It can process payments, but its methods (executeTransaction(), checkStatus(), getReferenceNumber()) do not match what the checkout service expects.
The translator. It implements the Target interface and holds a reference to the Adaptee, delegating calls with the necessary translation.
In our example, LegacyGatewayAdapter implements PaymentProcessor and wraps LegacyGateway, translating processPayment() into executeTransaction() and converting the long reference number into a String transaction ID.
The code that uses the Target interface. It is completely unaware of the Adaptee or the Adapter's internal workings.
Here is the Adapter workflow, step by step:
The client holds a reference typed as Target (e.g., PaymentProcessor). It calls a method like processPayment(amount, currency) without knowing what is behind the interface.
The adapter implements Target, so it receives the method call. It now needs to translate this into something the adaptee understands.
The adapter maps the Target method to the corresponding Adaptee method. This may involve renaming the method, reordering parameters, converting types, or combining multiple adaptee calls into one.
The adaptee runs its own logic, unaware that it was called through an adapter. It returns results in its own format.
If the return types differ, the adapter converts the adaptee's result into the format the Target interface expects and returns it to the client.
To integrate the legacy LegacyGateway class into our modern e-commerce system, we’ll create an object adapter called LegacyGatewayAdapter.
This adapter will implement the PaymentProcessor interface, which our CheckoutService already depends on.
Internally, it will translate method calls into the appropriate operations on the LegacyGateway effectively bridging the gap between incompatible APIs.
The beauty of the Adapter Pattern is that your client code remains completely unaware of the legacy integration.
The CheckoutService doesn’t care whether it’s processing a modern or legacy payment, it always talks to PaymentProcessor.
Here’s how the updated client code looks:
The adapter wraps LegacyGateway instead of subclassing it. This keeps the adapter loosely coupled, easy to test, and flexible enough to adapt any class that provides similar functionality.
Each method in PaymentProcessor is translated into the equivalent call on the legacy API. This includes renaming methods (processPayment becomes executeTransaction), bridging parameter differences (isPaymentSuccessful() must supply the reference number to checkStatus(long)), and converting return types (long to String).
The legacy gateway returns a long reference number. The adapter converts it to a String transaction ID by prefixing it with "LEGACY_TXN_". The client never sees the raw long.
The adapter shields the rest of the codebase from the legacy API's quirks. If the vendor releases a new version with different method names, only the adapter changes.
Lets say you are building a media player that natively plays MP3 files. The product team wants to add support for VLC (can play both MP4 and AVI) and MP4 formats. Rather than rewriting the player, you will use adapters to integrate external codec libraries.
The AudioPlayer works exclusively with the MediaPlayer interface. MP3 files play natively. MP4 and VLC files play through adapters that translate the play() call into the codec-specific method. Adding a new format (say, FLAC) means writing a new adapter, not modifying the AudioPlayer.