Explore the design pattern adapter with real-world TypeScript examples. Learn to bridge incompatible APIs, refactor legacy code, and build scalable systems.
January 7, 2026 (2mo ago)
Your Guide to the Design Pattern Adapter in Clean Code
Explore the design pattern adapter with real-world TypeScript examples. Learn to bridge incompatible APIs, refactor legacy code, and build scalable systems.
← Back to blog
The Adapter design pattern is essentially a go-between. Think of it as a translator that lets two incompatible systems talk to each other without a hitch. The whole point is to get existing classes to cooperate without ever touching their original source code. It’s the software equivalent of a travel adapter that lets you plug your Canadian electronics into a European outlet.
This pattern is a cornerstone of clean, maintainable code, especially when you’re trying to integrate a third-party library or wrangle a legacy system.1
Why Your Codebase Needs the Adapter Pattern

Picture this: you’re in Europe, trying to plug your Canadian laptop into the wall. It just won’t fit. Your charger works fine, the wall socket works fine, but their interfaces are completely different. This exact problem happens all the time in software development when we need two systems, never designed to meet, to suddenly work together.
That’s where the Adapter pattern comes in. It’s your universal travel adapter for code, creating a seamless bridge between mismatched components.
Bridging the Gap in Modern Development
Today we rarely build everything from scratch. We assemble solutions from third-party libraries, external APIs, and legacy systems. This speeds delivery but creates incompatible interfaces and tempting shortcuts that create long-term pain:
- Code duplication: the same translation logic ends up scattered across the app.
- High coupling: business logic becomes tangled with external service details.
- Rising technical debt: every API change sparks a scavenger hunt for brittle integration code.
The Adapter pattern gives you a dedicated adapter class that handles translation in one place, protecting your core application logic and making integrations easier to maintain. This approach is common in teams that prioritize modular architecture and gradual modernization.2
The Adapter pattern isn’t just a hack to make things work. It protects the simplicity and integrity of your core application logic by shielding it from the messy details of external systems.
Adapters Promote Clean and Scalable Code
An adapter is a strategy for building a flexible architecture that can change without a complete rewrite. You can swap services or phase out legacy systems by adding or replacing adapters rather than touching client code. That stability speeds feature development and reduces integration risk.2
How the Adapter Pattern Really Works
At its heart, the pattern creates a clean separation between parts of your system so they can evolve independently. Four players make this pattern easy to understand:
- The Client: the code that needs something done and expects a simple, stable interface.
- The Target: the clean interface the Client speaks.
- The Adaptee: the existing component with an incompatible interface (often unchangeable).
- The Adapter: implements the Target and translates calls to the Adaptee.

The Client only ever talks to the Target interface; the Adapter quietly translates for the Adaptee. This design aligns well with the Open/Closed Principle: you can integrate a new service by writing a new adapter while leaving existing client code untouched.1
The primary goal of the Adapter pattern is to convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.3
Building Adapters in TypeScript With Real Examples

Below are two practical TypeScript examples that reflect common real-world problems: adapting a legacy XML API and standardizing a third-party payment gateway.
Example 1: Adapting a Legacy XML API to JSON
Scenario: your modern React frontend expects JSON, but the only data source is a legacy service that returns XML. The client should use a clean IUserService interface; the LegacyUserService speaks XML. The adapter bridges them.
The Incompatible Adaptee
// Adaptee: The old service with an incompatible interface
class LegacyUserService {
fetchUsersXML(): string {
return `
<users>
<user id="1">
<name>Alice</name>
<email>alice@example.com</email>
</user>
<user id="2">
<name>Bob</name>
<email>bob@example.com</email>
</user>
</users>
`;
}
}
The Target Interface and Adapter
interface IUser {
id: number;
name: string;
email: string;
}
interface IUserService {
getUsers(): Promise<IUser[]>;
}
class UserServiceAdapter implements IUserService {
private adaptee: LegacyUserService;
constructor(legacyService: LegacyUserService) {
this.adaptee = legacyService;
}
async getUsers(): Promise<IUser[]> {
const xmlData = this.adaptee.fetchUsersXML();
// Use a robust XML parser in production (e.g., xml2js).
console.log("Translating XML to JSON...");
return [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
];
}
}
With this adapter, client code talks to IUserService and stays agnostic to XML.
Example 2: Standardizing a Third-Party Payment Gateway
Scenario: your app uses a standard IPaymentProcessor interface, but the PayWizard gateway has methods like startTransaction and verifyPaymentStatus. The adapter maps your standard calls to PayWizard’s API.
The Inconsistent Adaptee
class PayWizard {
startTransaction(amount: number, cardDetails: string): string {
console.log(`PayWizard: Initiating transaction for $${amount}.`);
const transactionId = "pw_" + Math.random().toString(36).substr(2, 9);
return transactionId;
}
verifyPaymentStatus(transactionId: string): boolean {
console.log(`PayWizard: Verifying status for ${transactionId}.`);
return true;
}
}
The Target Interface and Adapter
interface IPaymentProcessor {
processPayment(amount: number, cardInfo: string): Promise<string>;
checkStatus(id: string): Promise<boolean>;
}
class PayWizardAdapter implements IPaymentProcessor {
private payWizard: PayWizard;
constructor() {
this.payWizard = new PayWizard();
}
async processPayment(amount: number, cardInfo: string): Promise<string> {
console.log("Adapter: Translating 'processPayment' to 'startTransaction'.");
return this.payWizard.startTransaction(amount, cardInfo);
}
async checkStatus(id: string): Promise<boolean> {
console.log("Adapter: Translating 'checkStatus' to 'verifyPaymentStatus'.");
return this.payWizard.verifyPaymentStatus(id);
}
}
Using adapters keeps your application code clean and consistent across different providers.
Refactoring Legacy Code With Adapters

Every project eventually faces legacy code. The Adapter pattern lets you avoid risky big rewrites by wrapping old systems with a modern Target interface and migrating client code incrementally. This approach reduces risk and supports a controlled modernization plan.2
A Step-by-Step Migration Plan
- Define your ideal Target interface.
- Create the Adapter class that implements that interface and accepts the legacy Adaptee.
- Implement translation logic inside the adapter.
- Migrate client code incrementally to use the Adapter.
Accelerating Refactoring With AI
Modern AI tools can speed up boilerplate, letting you focus on the critical mapping logic, but the architectural decisions remain the team’s responsibility. Use tools to scaffold adapters and then write tests that validate the mappings.
Using an adapter isn’t just a temporary fix; it’s a strategic investment in architectural health that enables incremental modernization.2
Choosing Between Adapter and Other Patterns
Selecting the right pattern matters. Adapter, Decorator, Proxy, and Façade can look similar but serve different intents. Use the Adapter to change interfaces; use Decorator to add behavior; use Proxy to control access; use Façade to simplify complex subsystems.
Adapter vs Decorator
Adapter translates an interface. Decorator adds responsibilities while preserving the original interface.
Adapter vs Proxy
Proxy keeps the same interface and controls access or adds lazy initialization, caching, or logging. Adapter changes the interface so the client can use an otherwise incompatible component.
Adapter vs Façade
Façade simplifies a subsystem behind a single interface. Adapter focuses on converting a single object’s interface so two components can interoperate.
| Pattern | Primary Intent | When to Use |
|---|---|---|
| Adapter | Convert one interface to another | When an existing class must work with an incompatible client. |
| Decorator | Add responsibilities | When you want to extend behavior dynamically. |
| Proxy | Control access | When you need lazy loading, access control, or logging. |
| Façade | Simplify a subsystem | When you want a single entry point to complex behavior. |
Bringing the Adapter Pattern to Your Team
To avoid overuse, set clear guardrails for adapter creation:
- Documentation: each adapter needs a README stating the Adaptee, the Target interface, and the mapping.
- Testing: require unit tests that assert the translation logic.
- Performance monitoring: benchmark critical adapters when they sit in hot paths.
Add automated checks to enforce these rules in CI and keep adapters consistent across the codebase. Provide examples and templates on your internal docs site or link to guides like modernizing legacy systems.
Got Questions? Let’s Talk Adapters
Q&A
Q: When is an adapter better than a full rewrite?
A: Use an adapter when the existing component works but its interface doesn’t match your needs—especially for stable legacy systems or third-party APIs you don’t control. If the component is buggy or lacks required features, a rewrite may be warranted.
Q: Do adapters add noticeable performance overhead?
A: Adapters add a tiny overhead—an extra method call or a conversion step—but in most business applications this is negligible compared with network or I/O costs. For latency-sensitive systems, benchmark the adapter.
Q: How should my team test adapters?
A: Write unit tests focusing on the mapping between Target and Adaptee. Mock the Adaptee where appropriate and include integration tests to ensure the adapter behaves correctly with the real dependency.
AI writes code.You make it last.
In the age of AI acceleration, clean code isn’t just good practice — it’s the difference between systems that scale and codebases that collapse under their own weight.