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. For example, a PaymentProcessor
might use if-else
logic to handle both a modern StripeService
and a legacy BankTransferAPI
.
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’re building the checkout component of an e-commerce application.
Your Checkout Service is designed to work with a Payment 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, widely used and battle-tested… but with a completely different interface.
Here’s what that legacy payment class looks like:
You now have two incompatible interfaces. Your existing CheckoutService
expects a PaymentProcessor
. But LegacyGateway
does not implement it and it’s methods and signatures don't match:
processPayment(double)
vs. executeTransaction(double, String)
isPaymentSuccessful()
vs. checkStatus(long)
getTransactionId()
vs. getReferenceNumber()
(and their types are different too!)And here’s the challenge:
CheckoutService
— it’s used system-wide and tied to the PaymentProcessor
interface.LegacyGateway
— it’s from an external vendor.What 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 acts as a bridge between an incompatible interface and what the client actually expects.
It allows your system to remain flexible, extensible, and decoupled, without having to modify existing client code or third-party libraries.
Your application expects one interface (PaymentProcessor
), but the legacy system provides another (LegacyGateway
). The adapter allows the two to work together without altering either side.
There are two primary ways to implement an adapter, depending on the language and use case:
In our case, we’ll use the Object Adapter pattern to adapt the LegacyGateway
to the PaymentProcessor
interface.
PaymentProcessor
): The interface that the client code expects and uses.LegacyGateway
): The existing class with an incompatible interface that needs adapting.CheckoutService
): The part of your system that uses the Target interface.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.
For our example:
CheckoutService
)LegacyGateway
)LegacyGatewayAdapter
)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:
We use object composition, not inheritance. The adapter wraps the LegacyGateway
instead of subclassing it. This keeps the adapter:
It also follows effective Java best practices for working with third-party or legacy code.
Each method in PaymentProcessor
is translated into equivalent calls to the legacy API. This often includes:
long
transaction reference into a formatted String
IDThe adapter shields the rest of your codebase from the quirks or structure of the legacy class. From the outside, no one knows — or cares — that a legacy system is being used.
This improves: