AlgoMaster Logo

Adapter Design Pattern

Ashish

Ashish Pratap Singh

5 min read

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:

  • You’re integrating with a legacy system or a third-party library that doesn’t match your current interface.
  • You want to reuse existing functionality without modifying its source code.
  • You need to bridge the gap between new and old code, or between systems built with different interface designs.

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.

1. The Problem: Incompatible Payment Interfaces

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.

The Expected Interface

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 In-House Implementation

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:

  • You can’t change CheckoutService — it’s used system-wide and tied to the PaymentProcessor interface.
  • You can’t modify LegacyGateway — it’s from an external vendor.
  • But you must make them work together.

What you need is a translator — a class that sits between CheckoutService and LegacyGatewayadapting the incompatible interface into one that works with your system.

This is exactly what the Adapter Design Pattern does.

2. What is the Adapter Pattern

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.

Two Types of Adapters

There are two primary ways to implement an adapter, depending on the language and use case:

1. Object Adapter (Preferred in Java)

  • Uses composition: the adapter holds a reference to the adaptee (the object it wraps).
  • Allows flexibility and reuse across class hierarchies.
  • This is the most common and recommended approach in Java.

2. Class Adapter (Rare in Java)

  • Uses inheritance: the adapter inherits from both the target interface and the adaptee.
  • Requires multiple inheritance, which Java doesn’t support for classes.
  • More suitable for languages like C++.

In our case, we’ll use the Object Adapter pattern to adapt the LegacyGateway to the PaymentProcessor interface.

Class Diagram

  • Target Interface (e.g., PaymentProcessor): The interface that the client code expects and uses.
  • Adaptee (e.g., LegacyGateway): The existing class with an incompatible interface that needs adapting.
  • Adapter: The class that implements the Target interface and uses the Adaptee internally. It translates calls on the Target interface into calls on the Adaptee's interface.
  • Client (e.g., CheckoutService): The part of your system that uses the Target interface.

3. Implementing Adapter

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 Adapter Implementation

Client Code Remains Unchanged

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:

What Makes This Adapter Work?

Composition Over Inheritance

We use object composition, not inheritance. The adapter wraps the LegacyGateway instead of subclassing it. This keeps the adapter:

  • Loosely coupled
  • Easier to test
  • More flexible to change

It also follows effective Java best practices for working with third-party or legacy code.

Method Translation

Each method in PaymentProcessor is translated into equivalent calls to the legacy API. This often includes:

  • Renaming or remapping method names
  • Reorganizing parameters
  • Converting return types — e.g., converting a long transaction reference into a formatted String ID

Encapsulation of Legacy Logic

The 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:

  • Encapsulation
  • Code readability
  • Maintainability