Adapter Design Pattern in Java: Making Incompatible Interfaces Work Together

Description / Meta Description

Learn the Adapter Design Pattern in Java with practical examples. Understand how Adapter helps integrate legacy systems, third-party APIs, and incompatible interfaces without modifying existing code. Discover real-world examples from Spring, Java libraries, and enterprise applications.


Adapter Design Pattern in Java: Making Incompatible Interfaces Work Together

In the previous articles, we completed all five Creational Design Patterns:

  • Singleton
  • Factory Method
  • Builder
  • Abstract Factory
  • Prototype

Now we begin the second major Gang of Four category:

Structural Design Patterns

While Creational patterns focus on:

How objects are created

Structural patterns focus on:

How objects are organized
How classes collaborate
How systems integrate

We’ll start with one of the most practical and commonly encountered patterns in enterprise applications:

Adapter Pattern

If you’ve ever integrated:

  • Legacy systems
  • Third-party APIs
  • External payment gateways
  • Different versions of services

then you’ve likely encountered a situation where Adapter can save you.


The Problem: Incompatible Interfaces

Imagine you’re building a payment processing system.

Your application expects:

public interface PaymentGateway {

    void pay();
}

Your service code uses:

PaymentGateway gateway;

gateway.pay();

Everything looks clean.


Now a business requirement arrives.

You must integrate a legacy payment provider.

Unfortunately their API looks like:

public class LegacyPaymentSystem {

    public void makePayment() {

        System.out.println(
            "Legacy Payment Executed");
    }
}

Notice the problem.

Your system expects:

pay()

Legacy system provides:

makePayment()

Interfaces don’t match.


Common Bad Solutions

Many developers immediately modify client code.

Example:

if(provider.equals("NEW")) {

    gateway.pay();
}
else {

    legacy.makePayment();
}

Soon:

if
else if
else if
else if

The code becomes harder to maintain.


What We Really Want

Ideally:

Application
      │
      ▼
PaymentGateway Interface
      │
      ▼
Works With Everything

Without modifying:

  • Client code
  • Legacy system
  • Existing contracts

This is exactly what Adapter Pattern provides.


What is Adapter Pattern?

Adapter is a Structural Design Pattern that:

Converts the interface of one class into another interface that clients expect.

In simpler terms:

Application Speaks English

Legacy System Speaks French

Adapter Translates

Think of:

Travel Plug Converter

US Plug
   │
Adapter
   │
European Socket

The devices remain unchanged.

The Adapter bridges the gap.


Architecture Diagram

Client
   │
   ▼
Target Interface
   │
   ▼
Adapter
   │
   ▼
Adaptee (Legacy System)

Step 1: Define Target Interface

The interface expected by the application.

public interface PaymentGateway {

    void pay();
}

Step 2: Existing Legacy Class

public class LegacyPaymentSystem {

    public void makePayment() {

        System.out.println(
            "Legacy Payment Executed");
    }
}

We cannot modify this class.

Maybe:

  • Vendor owns it
  • Third-party library
  • Legacy production system

Step 3: Create Adapter

public class PaymentAdapter
        implements PaymentGateway {

    private LegacyPaymentSystem
            legacySystem;

    public PaymentAdapter(
            LegacyPaymentSystem legacySystem) {

        this.legacySystem = legacySystem;
    }

    @Override
    public void pay() {

        legacySystem.makePayment();
    }
}

Notice what happened.

The Adapter translates:

pay()
      ↓
makePayment()

Step 4: Client Code

LegacyPaymentSystem legacy =
        new LegacyPaymentSystem();

PaymentGateway gateway =
        new PaymentAdapter(legacy);

gateway.pay();

Output:

Legacy Payment Executed

The application remains unaware of the legacy implementation.

Mission accomplished.


Why Adapter Works

Without Adapter:

Client
   │
   ▼
Legacy System

Problem:
Interfaces incompatible

With Adapter:

Client
   │
   ▼
Adapter
   │
   ▼
Legacy System

Adapter absorbs the complexity.


Real Enterprise Example: SMS Providers

Suppose your application expects:

public interface SmsService {

    void send(String message);
}

Provider A:

sendSMS()

Provider B:

pushMessage()

Provider C:

transmit()

Without Adapter:

Client must know every provider

Bad design.


Using Adapters:

SmsAdapterA
SmsAdapterB
SmsAdapterC

All expose:

send()

Client remains unchanged.


Adapter in Spring Boot Applications

Consider a notification service.

Application contract:

public interface NotificationService {

    void sendNotification();
}

Current implementation:

EmailNotificationService

Tomorrow:

Slack
Teams
WhatsApp
Telegram

Instead of changing business logic, adapters translate external APIs into a common internal contract.

This keeps services clean and loosely coupled.


Object Adapter vs Class Adapter

GoF describes two approaches.


1. Object Adapter (Most Common)

Uses composition.

Adapter
    HAS-A
Legacy Object

Example:

private LegacyPaymentSystem legacy;

Benefits:

✔ Flexible

✔ Preferred in Java

✔ Works with third-party classes


2. Class Adapter

Uses inheritance.

Adapter
    IS-A
Legacy Class

Example:

public class Adapter
    extends LegacyClass

Less common.

Java’s single inheritance limits flexibility.

Most modern Java systems prefer Object Adapter.


Real World Examples You Already Use

Many developers unknowingly use Adapter every day.


Example 1: Spring MVC

Spring converts:

HTTP Request

into:

@RequestBody UserRequest

Spring acts like an Adapter.


Example 2: SLF4J

Application code:

Logger logger;

Underlying implementation:

Logback
Log4j
JUL

SLF4J adapts multiple logging frameworks.


Example 3: JDBC Drivers

Application code:

Connection connection;

Actual drivers:

Oracle
MySQL
Postgres
SQL Server

Drivers adapt vendor-specific implementations to standard JDBC contracts.


Example 4: Spring Security

Authentication providers often adapt:

LDAP
Database
OAuth
SAML

into a common authentication contract.


Benefits of Adapter Pattern

1. Reuse Existing Code

No need to rewrite legacy systems.


2. Loose Coupling

Clients depend on interfaces.

Not implementations.


3. Easy Integration

Third-party systems become easier to consume.


4. Open/Closed Principle

Add new adapters without modifying existing code.


5. Better Maintainability

Integration logic stays isolated.


Common Mistakes


Mistake 1: Putting Business Logic Inside Adapter

Adapter should translate interfaces.

Not implement business workflows.

Bad:

Adapter
   ├── Translation
   ├── Validation
   ├── Billing
   └── Reporting

Keep responsibilities focused.


Mistake 2: Adapter Explosion

Creating dozens of unnecessary adapters can complicate systems.

Use only when interface mismatch exists.


Mistake 3: Ignoring Existing Contracts

Sometimes a simple interface change solves the problem.

Don’t introduce Adapter unnecessarily.


Adapter vs Facade

A common interview question.


Adapter

Purpose:

Make incompatible interfaces compatible

Example:

pay()
     ↓
makePayment()

Facade

Purpose:

Simplify a complex subsystem

Example:

Many APIs
    ↓
Single API

Adapter vs Decorator

Another common confusion.


Adapter

Changes interface.

Interface A
    ↓
Interface B

Decorator

Keeps interface.

Adds behavior.

Existing Object
      ↓
Additional Features

Quick Comparison

PatternPurpose
AdapterMake incompatible interfaces work together
DecoratorAdd behavior dynamically
FacadeSimplify complex systems
ProxyControl access

When Should You Use Adapter?

Use Adapter when:

✔ Integrating legacy systems

✔ Consuming third-party APIs

✔ Migrating platforms

✔ Supporting multiple providers

✔ Interface mismatch exists


Avoid Adapter when:

❌ Interfaces already align

❌ Simpler refactoring solves the issue

❌ Additional abstraction adds no value


Final Thoughts

The Adapter Pattern solves one of the most common integration problems in software engineering:

How do we make two systems work together without changing either one?

By acting as a translation layer, Adapter enables:

  • Legacy integration
  • Third-party API consumption
  • Cleaner architecture
  • Reduced coupling

This is why Adapter appears frequently in:

  • Spring Framework
  • JDBC
  • Logging frameworks
  • Security integrations
  • Enterprise applications

In the next article, we’ll continue our Structural Pattern journey with another highly practical pattern:

Decorator Pattern — Adding New Behavior to Objects Without Modifying Their Code

You’ll discover why Java I/O streams, Spring components, and many modern frameworks rely heavily on this elegant pattern.

Leave a Reply

Your email address will not be published. Required fields are marked *