Prologue: What a Travel Adapter Taught Me About Software
I was on a trip to the US. I arrived at my hotel room, ready to charge my Korean phone. I pulled out my charger and approached the wall socket.
Then I froze. The plug didn't fit.
Korea uses 220v two-prong plugs. The US uses 110v three-prong sockets. Physically incompatible.
Panicking, I went down to the hotel lobby and asked, "Do you have an adapter?" The receptionist smiled and handed me a Travel Adapter.
That's when it hit me. "This is the Adapter Pattern in software."
The Essence of Adapter Pattern: A Converter
The Adapter Pattern is a converter that connects incompatible interfaces.
- Phone Charger (Original System): Korean two-prong plug.
- US Wall Socket (New System): Three-prong socket.
- Travel Adapter (Adapter): Intermediary that converts two-prong to three-prong.
In code terms:
- Legacy Code: Old system (10-year-old API).
- New API: Modern system (latest library).
- Adapter Class: Translation layer connecting the two.
Why Do We Need This? (My Real Problem)
I was upgrading the payment system for an e-commerce platform I ran.
The Legacy System (10-Year-Old Code)
A decade ago, an outsourced vendor built a PaymentService class.
class OldPaymentService {
void payCash(int cents) {
// Payment in cents
System.out.println("Payment: " + cents + " cents");
}
}
This code was embedded everywhere.
Order processing, refund logic, settlement systems... at least 30 files called payCash.
The New Payment API (Provided by PG Company)
Our Payment Gateway (PG) company offered a new API. 30% lower fees. International payment support.
The catch? Different interface:
interface ModernPaymentGateway {
void processPayment(double dollars);
}
- Old API:
payCash(int cents)- Cents unit, int type. - New API:
processPayment(double dollars)- Dollar unit, double type.
My First Idea: Rewrite Everything (Worst Approach)
"Let's just open all 30 files and replace payCash(cents) with processPayment(dollars)."
But modifying 30 files meant:
- Bug Risk: One mistake = payment failure.
- Testing Hell: Re-test every code path in 30 files.
- Time: At least a week.
And if we later adopt another payment system? Modify those 30 files again.
The Realization: Just Add One Adapter
"Don't touch the existing code. Just insert a translation layer (Adapter)."
class PaymentAdapter implements ModernPaymentGateway {
private OldPaymentService oldService;
public PaymentAdapter(OldPaymentService old) {
this.oldService = old;
}
@Override
public void processPayment(double dollars) {
// 1. Convert dollars to cents
int cents = (int) (dollars * 100);
// 2. Delegate to old system
oldService.payCash(cents);
}
}
Now I don't touch those 30 files. Just plug this adapter into the new API.
// Before: Old code
OldPaymentService payment = new OldPaymentService();
payment.payCash(5000); // $50 = 5000 cents
// After: Adapter applied
ModernPaymentGateway payment = new PaymentAdapter(new OldPaymentService());
payment.processPayment(50.00); // $50
3 Core Values of Adapter Pattern
1. Don't Touch Existing Code (Open/Closed Principle)
OldPaymentService class: Not a single line modified.
Touching verified code introduces new bugs.
Adapters follow the SOLID principle: "Open for extension, closed for modification."
2. Minimize Testing Scope
The existing code (OldPaymentService) has been battle-tested for 10 years.
Only the adapter needs testing.
@Test
void testAdapter() {
PaymentAdapter adapter = new PaymentAdapter(new OldPaymentService());
adapter.processPayment(50.00);
// Output: "Payment: 5000 cents"
// ✅ Just verify dollar→cent conversion
}
3. Future Scalability
If 6 months later we add another payment provider (e.g., Stripe)? Just create one more adapter:
class StripeAdapter implements ModernPaymentGateway {
private StripeAPI stripe;
@Override
public void processPayment(double dollars) {
stripe.charge(dollars);
}
}
The old code (OldPaymentService, 30 files)? Still untouched.
Real Experience: XML → JSON Conversion
Another project where I used the Adapter Pattern.
The Problem
Our internal company system used XML format for data exchange (2010s legacy).
class LegacySystem {
String getDataAsXML() {
return "<user><name>John</name></user>";
}
}
But our new web app only understood JSON.
// React component expects JSON
fetch('/api/user')
.then(res => res.json()) // Expects JSON
.then(data => console.log(data.name));
Solution: XML → JSON Adapter
class XmlToJsonAdapter {
private LegacySystem legacy;
public XmlToJsonAdapter(LegacySystem legacy) {
this.legacy = legacy;
}
public String getDataAsJSON() {
String xml = legacy.getDataAsXML();
// Parse XML, convert to JSON
return "{\"name\": \"John\"}";
}
}
At the API layer:
@GetMapping("/api/user")
public String getUser() {
XmlToJsonAdapter adapter = new XmlToJsonAdapter(new LegacySystem());
return adapter.getDataAsJSON(); // Returns JSON
}
Now React gets JSON, and the legacy system (LegacySystem) remains unchanged.
Adapter vs Other Patterns
Adapter vs Decorator (Confusing Part)
Both "wrap an existing object", but:
| Pattern | Purpose | Example |
|---|---|---|
| Adapter | Interface conversion. Make incompatible things compatible. | 110v → 220v converter |
| Decorator | Add功能. Add new features to existing object. | Add milk to coffee |
When NOT to Use Adapter
Anti-Pattern: Overusing Adapters
I made this mistake. "Adapters are great!" So I wrapped everything.
// ❌ Unnecessary adapter
class StringAdapter {
private String str;
public String getString() { return str; }
}
This is just a wrapper. If there's no conversion, it's not an adapter.
When to Use Adapter
- Connecting legacy system with new system.
- Third-party library interface doesn't match your code.
- Cannot modify one side (external API, already deployed library).
Code Example: TypeScript in Production
Actual code from my Next.js blog.
Problem
Supabase returns dates in ISO 8601 format (2025-06-03T00:00:00Z).
But my UI component only understands YYYY-MM-DD.
Adapter
class DateAdapter {
constructor(private isoDate: string) {}
getFormattedDate(): string {
return this.isoDate.split('T')[0]; // "2025-06-03"
}
}
// Usage
const post = await supabase.from('posts').select('created_at').single();
const adapter = new DateAdapter(post.created_at);
console.log(adapter.getFormattedDate()); // "2025-06-03"
Supabase code (created_at format) untouched. My UI gets the format it wants.
Deep Dive: Object Adapter vs Class Adapter
In design patterns theory (GoF), there are actually two types of Adapters.
1. Object Adapter (Recommended)
This is what I showed above. It uses Composition. The adapter holds an instance of the legacy class.
class Adapter implements Target {
private Legacy legacy; // Composition
public Adapter(Legacy legacy) {
this.legacy = legacy;
}
}
- Pros: Flexible. Can work with subclasses of Legacy.
- Cons: Need to instantiate the legacy object.
2. Class Adapter (Not Recommended)
This uses Multiple Inheritance (C++ style). In Java, it uses extends.
// Logic: "I am both a Target AND a Legacy"
class Adapter extends Legacy implements Target {
@Override
public void request() {
super.specificRequest();
}
}
- Pros: Overrides legacy methods easily.
- Cons: Inheritance is rigid. Breaks encapsulation.
My Verdict: Always use Object Adapter. Composition over Inheritance!
Connection to Liskov Substitution Principle (LSP)
The Adapter Pattern is the ultimate cheat code for LSP. LSP says: "Subtypes must be substitutable for their base types."
If you have a LegacyService that behaves wildly differently from NewService, you can't just inherit.
The Adapter acts as a Translator that forces the Legacy code to behave like a respectful subtype of the new interface.
It smooths out the rough edges so the rest of your system can treat everything uniformly (Polymorphism).
Summary: Pros and Cons
| Feature | Description |
|---|---|
| Single Responsibility | ✅ Separates the interface conversion logic from business logic. |
| Open/Closed | ✅ Adds new behavior without changing old code. |
| Complexity | ❌ Increases the number of classes. "Why do we have so many files?" |
| Performance | ⚠️ Slight overhead (method forwarding), but negligible in modern CPUs. |
Real World Example: Java JDBC
If you are a Java developer, you use adapters every day. JDBC (Java Database Connectivity) is one giant Adapter Pattern.
- Target Interface:
java.sql.Connection,java.sql.ResultSet. - Adaptee: Oracle Driver, MySQL Driver, PostgreSQL Driver.
- Adapter: The JDBC Driver Implementation.
You write code against java.sql.* interfaces.
You never touch the Oracle proprietary API directly.
The Driver adapts the Oracle API to the Standard SQL Interface.
This allows you to switch databases by changing one configuration line (Driver Class Name).
Python Example: Duck Typing Adapter
In Python, we don't strictly need interfaces, but the pattern is still useful.
class Dog:
def bark(self):
return "Woof!"
class Cat:
def meow(self):
return "Meow!"
class CatAdapter:
def __init__(self, cat):
self.cat = cat
def bark(self):
# Translate meow to bark
return self.cat.meow()
def make_noise(animal):
print(animal.bark())
dog = Dog()
cat = Cat()
adapted_cat = CatAdapter(cat)
make_noise(dog) # Woof!
make_noise(adapted_cat) # Meow! (But called via bark method)
Even in dynamic languages, adapting method names meow() -> bark() allows polymorphism.
FAQ
Q: Is Adapter same as Bridge Pattern? A: No.
- Adapter: Makes things work after they are designed. (Retrofitting)
- Bridge: Designs things to work independently before they are built. (Upfront design)
Q: Can I use it to wrap multiple classes? A: Yes! That's called a Facade sometimes, but if it respects an interface, it's an Adapter. You can adapt a whole subsystem to a single interface.
Final Thought: "Don't Rewrite, Wrap"
When I first learned coding, I thought "If it's wrong, fix it!" But in production, I learned: "If it works, don't touch it."
The Adapter Pattern is this philosophy implemented.
- Existing code: Leave it alone (Zero bug risk).
- New requirement: Wrap with adapter.
- Testing: Focus only on adapter.
Just like packing a travel adapter for your trip, pack an adapter in your codebase. It saves you from rewriting the world.
"The best refactoring is often no refactoring at all. Just adapt."