Adapter Pattern
Learn how the Adapter Pattern acts as a bridge between incompatible interfaces, allowing your codebase to stay clean and decoupled while integrating third-party libraries.
Welcome to the second major category of design patterns: Structural Design Patterns.
While creational patterns focus on how objects are born, structural patterns focus on how classes and objects are composed to form larger structures.
The Essentials
- Bridge the Gap: Use an Adapter when you have two incompatible interfaces that need to work together.
- Decouple Third Parties: Never let external SDKs or APIs leak into your core business logic.
- Implement a Local Interface: Define what your app needs and map the third-party methods to it inside the Adapter.
Today, we dive into one of the most practical structural patterns: the Adapter Design Pattern.
1. What is an Adapter?
To understand this pattern, look no further than the real-world Power Adapter or a MacBook Dongle.
The Analogies
- Power Adapter: If you buy a laptop from the US, its plug won't fit into an Indian socket. You don't rewrite the laptop's power system, and you don't rewire your house. Instead, you use a "converter," an adapter that sits in the middle.
- MacBook Dongle: Modern MacBooks only have USB-C ports. If you want to connect an HDMI cable, you use an adapter. Apple engineers only need to write code to read from one port (USB-C), and the adapter handles the logic of converting HDMI signals into something the MacBook understands.
The Purpose
An adapter has two main goals:
- Single Interface Support: Your main codebase only needs to handle one type of "plug" (interface).
- Conversion Logic: It handles the translation from a "third-party" interface to the one your system expects.
2. Why do we need it in Software?
In software engineering, we rarely write every single line of code ourselves. We depend on Third-Party Libraries for logging, payments, database connections, and more.
The "Direct Connection" Problem
Imagine PhonePe directly importing a specific bank's library (e.g., YesBankLibrary) into their core classes:
class PhonePay {
private bank: YesBank;
public pay() {
this.bank.doTransaction(); // Tightly coupled!
}
}This violates the Dependency Inversion Principle. Your high-level PhonePay class is directly dependent on a concrete YesBank implementation. If Yes Bank's servers go down, or if you want to swap it for ICICI Bank, you have to rewrite your entire codebase.
The "Interface" Bottleneck
You might think: "I'll just create a Bank interface!"
interface Bank {
void processPayment();
}But here is the catch: Third-party vendors won't implement your interface.
MySQL won't implement a Database interface created by a random developer. Razorpay won't implement your custom PaymentGateway interface. They have their own proprietary methods and logic.
3. The Solution: The Adapter Bridge
Since the third party won't change for you, and you don't want to pollute your code with their specific methods, you build a Bridge: the Adapter.
How it Works
- Your Interface: You define an interface that your application needs (e.g.,
PaymentGatewaywithmakePayment()andcheckStatus()). - The Adaptee: This is the third-party SDK/API (e.g.,
RazorpayAPI) with its own proprietary methods. - The Adapter: A class that implements your interface. Internally, it holds an instance of the third-party API and delegates the work.
Implementation Example: Payment Gateway
// 1. The Interface OUR code knows
interface PaymentGateway {
makePayment(amount: number): void;
getStatus(id: string): string;
}
// 2. Third-Party API (Adaptee) - Proprietary logic
class RazorpayAPI {
public processTransaction(amt: number): void { /* ... */ }
public fetchStatus(txId: string): string { /* ... */ }
}
// 3. The Adapter - Mapping our methods to theirs
class RazorpayAdapter implements PaymentGateway {
private api: RazorpayAPI;
constructor() {
this.api = new RazorpayAPI();
}
public makePayment(amount: number) {
this.api.processTransaction(amount);
}
public getStatus(id: string) {
return this.api.fetchStatus(id);
}
}Implementation Example: Logging System
This example shows how we can use the Adapter Pattern with Dependency Injection to create a highly flexible logging system.
interface ILogger {
log(message: string): void;
}
// Third-party SDK with a different method name and parameter type
class Log4jSDK {
public sendStream(msg: string): void { /* ... */ }
}
class Log4jAdapter implements ILogger {
private sdk = new Log4jSDK();
public log(message: string) {
this.sdk.sendStream(message);
}
}
// Client code using Dependency Injection
class Application {
private logger: ILogger;
constructor(logger: ILogger) {
this.logger = logger;
}
public run() {
this.logger.log("Application started");
}
}4. The Benefits: OCP and DIP in Action
The Adapter Pattern isn't just about "converting" code; it's about Architectural Integrity.
- Dependency Inversion (DIP): Your core business logic (e.g.,
PhonePay) no longer depends on a concrete class likeYesBank. It depends on aBankinterface. You have inverted the dependency. - Open-Closed Principle (OCP): If you want to add a third payment gateway (like PayU), you don't touch your existing
PaymentServiceorRazorpayAdapter. You simply create aPayUAdapterthat implements the same interface. - Unified Experience: Your application code deals with a consistent "Contract" (the interface), while the messiness of third-party variations is hidden inside the adapters.
5. Synergy: Factory + Adapter
While the Adapter handles the incompatible interface, the Factory Pattern often handles the creation of these adapters.
class PaymentGatewayFactory {
public static getGateway(type: string): PaymentGateway {
if (type === "RAZORPAY") return new RazorpayAdapter();
if (type === "PAYU") return new PayUAdapter();
throw new Error("Invalid Gateway");
}
}This combination allows for Plug-and-Play architecture. You can switch your entire payment infrastructure at runtime just by changing a single config string.
Deep Dive: Adapter FAQs
1. Can't we just use Adapters without an Interface?
Technically, you could create classes like RazorpayAdapter and PayUAdapter without a common interface. However, you lose the Contract.
- No Enforcement: Without an interface, nothing forces you to implement
getStatus()in the next adapter. You might forget it or name it differently. - No Polymorphism: If you want to maintain a
List<PaymentGateway>to try multiple providers (like Torfate does for success rates), you must have a common interface.
2. Is there a strict naming convention?
Not strictly, but calling your class [ThirdParty]Adapter (e.g., Log4jAdapter) is a standard industry practice. It immediately tells other developers that this class is a bridge to an external dependency.
3. When should I NOT use an Adapter?
Always evaluate the trade-offs before adding an abstraction layer. If your project is a small, one-off script that will only ever use one library, adding an adapter might be over-engineering. Adapters add a small amount of extra work and code. Evaluate the "Pros and Cons": is the future extensibility worth the current effort? For industrial-grade systems, the answer is almost always Yes.
Summary
The Adapter Pattern is your best friend whenever you encounter a "Third-Party" dependency.
- Don't connect directly to external SDKs; they are volatile and beyond your control.
- Create a Buffer: Define an interface with the methods you need.
- Implement the Bridge: Write an Adapter class that wraps the external SDK.
By using adapters, you ensure that your codebase remains your own. External changes might break an adapter, but they will never break your core business logic.
In the next post, we'll explore the Facade Pattern, which helps us simplify complex systems by providing a single, unified interface to a set of interfaces in a subsystem.
Further Reading
- Refactoring Guru: Adapter Pattern
- Case Study: Explore how SLF4J acts as an adapter for various logging frameworks like Log4j and Logback.
Keep reading