Welcome back to The Everyday Backend Engineer: Practical Design Patterns. In the last post, we simplified complex workflows using the Facade Pattern. Today, we continue exploring Category 2: Structural Patterns with an indispensable tool for working with third-party vendor code: The Adapter Pattern.
Let’s look at how to cleanly plug conflicting external systems into your architecture without changing a single line of your core code.
🔴 The Problem: The Third-Party Compatibility Wall
Imagine you have an existing application with a well-established payment flow. Your system currently utilizes a legacy internal payment service that expects a standardized method name: .makePayment(amount).
// Our native client code expects this exact method structure
class CheckoutController {
async purchase(paymentService, amount) {
// ... internal logic
await paymentService.makePayment(amount); // 🎯 Standardized name
}
}
The vendor shift...
Your company decides to switch to a new payment provider, Khalti. You download their official SDK, but you discover their method signature is completely different. They don’t have a .makePayment() method; instead, their SDK expects .initiateTransaction(amount).
If you try to inject this directly into your system, your code breaks immediately because your application is expecting .makePayment(). To fix this without an architectural pattern, you would have to manually find every single location in your app where payments are processed and rewrite the function calls to match Khalti's specific SDK names.
🟢 The Solution: A Translation Bridge
Instead of forcing your entire application to change its language to match an external library, you build a translator class. This class is called an Adapter.
The Adapter implements the structural interface your application already expects (.makePayment()), intercepts that call, and internally translates it into the method name expected by the third-party SDK (.initiateTransaction()). Your native app code remains completely unaware that the underlying provider has changed.
🔌 The Real-World Analogy: "The Travel Adapter"
Think of traveling to another country with your laptop. Your laptop charger has a two-pin plug designed for a standard outlet. However, the hotel room wall only features a three-pin socket. You don't slice open your laptop cable and reweld the wires to match the country's grid. Instead, you buy a cheap Travel Adapter. The adapter accepts your native two-pin plug on one side and fits into the three-pin wall outlet on the other, acting as a harmless translation layer.
💻 The Clean Node.js Implementation
Let’s implement this cleanly by building a structural bridge between our application and the new payment SDK.
First, let's look at the third-party SDK code which we are not allowed to modify:
// node_modules/khalti-sdk (Simulated third-party code)
class KhaltiSDK {
initiateTransaction(amt) {
console.log(`💸 Khalti API Gateway actively charged: Rs. ${amt}`);
return { transactionStatus: 'SUCCESS' };
}
}
module.exports = KhaltiSDK;
Next, we create our structural Adapter class to translate our application's expectations into the language of the SDK:
// adapters/khaltiAdapter.js
const KhaltiSDK = require('khalti-sdk-placeholder'); // The incompatible vendor SDK
class KhaltiAdapter {
constructor() {
this.khalti = new KhaltiSDK(); // Encapsulate the raw SDK internally
}
// 🎯 Implement the method signature our native application code expects
async makePayment(amount) {
console.log("🔌 Adapter intercepting payment request, translating signatures...");
// Translate `.makePayment(amount)` down into `.initiateTransaction(amt)`
const response = this.khalti.initiateTransaction(amount);
// Return a unified response structure that your application understands
return { success: response.transactionStatus === 'SUCCESS' };
}
}
module.exports = KhaltiAdapter;
🎯 How to swap providers seamlessly without altering core logic
Look at how beautifully unbothered your main core logic stays. We can pass the new adapter right into the service, and the application keeps running smoothly without changing a single line of business logic:
// controllers/paymentController.js
const KhaltiAdapter = require('../adapters/khaltiAdapter');
class OrderService {
// Expects any target payment service that implements `.makePayment()`
async processOrderCheckout(paymentProcessor, orderAmount) {
console.log("Processing order checkout workflow...");
// 🚀 The app runs seamlessly because the adapter provides `.makePayment`
const transaction = await paymentProcessor.makePayment(orderAmount);
if (transaction.success) {
console.log("✅ Order finalized successfully!");
}
}
}
// Execution setup
const orderService = new OrderService();
const readyPaymentProcessor = new KhaltiAdapter(); // The adapter wrapper
// Trigger the purchase process seamlessly
orderService.processOrderCheckout(readyPaymentProcessor, 2500);
🧠 Code Smell Test: When should you use an Adapter?
Keep a sharp eye out for this indicator during development:
- The Smell: You are integrating a new third-party library or an updated package version, and you find yourself having to refactor existing variable names, function signatures, or response mappings inside your native business logic files to accommodate the new package structure.
- The Fix: Undo those changes. Wrap the new library inside an Adapter class that mirrors your original application design, and handle the conversions safely inside the wrapper.
That wraps up Step 7! Your code is now fully guarded against erratic changes from external vendors and third-party APIs.
Up next in our architectural journey is Step 8: The Repository Pattern, where we will look at how to cleanly isolate our database queries from our business logic workflows.
How do you manage third-party API changes in your applications? Do you write custom adapters, or do you let vendor SDK signatures bleed directly into your controllers? Let's discuss in the comments below!
Top comments (0)