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
| Pattern | Purpose |
|---|---|
| Adapter | Make incompatible interfaces work together |
| Decorator | Add behavior dynamically |
| Facade | Simplify complex systems |
| Proxy | Control 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.