Adapter Pattern
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise due to incompatible interfaces.
What is it?
The Adapter pattern acts as a bridge between two incompatible interfaces. It's like a universal power adapter that lets you plug your device into any outlet worldwide. In code, it wraps existing classes and makes them compatible with your app's expected interface.
The Problem: API Integration Complexity
Your app needs to process payments, but each provider has a completely different API format. The **Adapter Pattern** organizes this complexity cleanly, while **Direct Integration** scatters it everywhere, creating a maintenance nightmare.
🔍 Why Direct Integration Gets Complex
The Real Problem: Scattered Complexity
✅ With Adapter Pattern:
💀 With Direct Integration:
📁 The Scattered Code Problem
Duplicated Logic Everywhere
This is why Direct Integration becomes a maintenance nightmare. The same conversion logic gets duplicated in every file that handles payments:
OrderService.js
Duplicate payment logic// Must manually handle each provider
if (provider === 'stripe') {
stripe.charges.create({
amount: total * 100, // Manual conversion
currency: currency.toLowerCase()
});
} else if (provider === 'paypal') {
paypal.payment.create({
total: total.toString(), // Different conversion
currency_code: currency.toUpperCase()
});
}
// ... duplicate logic for each providerRefundService.js
Same conversion logic repeated// Same conversion logic duplicated!
if (provider === 'stripe') {
stripe.refunds.create({
charge: chargeId,
amount: refundAmount * 100 // Convert to cents again
});
} else if (provider === 'paypal') {
paypal.refund.create({
amount: refundAmount.toString() // Convert to string again
});
}
// ... more duplicate logicReportingService.js
Provider-specific formatting everywhere// Provider formatting scattered everywhere
function formatAmount(amount, provider) {
if (provider === 'stripe') {
return amount * 100; // Cents conversion again
} else if (provider === 'paypal') {
return amount.toString(); // String conversion again
} else if (provider === 'square') {
return { amount: amount * 100, currency: 'USD' }; // Nested object again
}
// ... same logic repeated in 15+ files
}SubscriptionService.js
More duplicate conversion logic// Yet another place with the same conversions
function processSubscription(amount, provider) {
if (provider === 'stripe') {
return stripe.subscriptions.create({
amount: amount * 100, // Cents conversion duplicated
currency: 'usd'
});
}
// ... same pattern repeated everywhere
}💥 The Problem:
- • Same conversion logic duplicated in 20+ files
- • Add new provider? Update every single file manually
- • Bug in conversion logic? Fix it in 20+ places
- • Different developers implement slightly different logic
- • High chance of missing updates in some files
Code Example: Clean vs Scattered
// Adapter Pattern - Complexity isolated and organized
interface PaymentProcessor {
processPayment(amount: number, currency: string): PaymentResult;
}
// All complexity hidden in specific adapters
class StripeAdapter implements PaymentProcessor {
processPayment(amount: number, currency: string) {
// Handle Stripe's specific requirements here
return StripeAPI.charges.create({
amount: amount * 100, // Stripe wants cents
currency: currency.toLowerCase() // Stripe wants lowercase
});
}
}
class PayPalAdapter implements PaymentProcessor {
processPayment(amount: number, currency: string) {
// Handle PayPal's specific requirements here
return PayPalAPI.payment.create({
total: amount.toString(), // PayPal wants string
currency_code: currency.toUpperCase() // PayPal wants uppercase
});
}
}
// Your business logic stays clean and simple!
class OrderService {
processPayment(processor: PaymentProcessor, amount: number) {
return processor.processPayment(amount, "USD"); // Clean interface!
}
}
// vs Direct Integration - Complexity scattered everywhere
class OrderService {
processPayment(provider: string, amount: number, currency: string) {
// Must handle every provider's specific format manually
if (provider === "stripe") {
return StripeAPI.charges.create({
amount: amount * 100, // Manual conversion
currency: currency.toLowerCase() // Manual formatting
});
} else if (provider === "paypal") {
return PayPalAPI.payment.create({
total: amount.toString(), // Different conversion
currency_code: currency.toUpperCase() // Different formatting
});
} else if (provider === "bitcoin") {
return BitcoinAPI.transactions.send({
amount_btc: amount / 45000, // Yet another conversion
currency: currency // Yet another format
});
}
// This same logic gets duplicated in RefundService, ReportingService,
// SubscriptionService, InvoiceService, and 15+ other files!
}
}
// RESULT:
// Adapter: Complexity organized in dedicated classes
// Direct: Same complexity duplicated in 20+ filesCommon Uses
- Integrating third-party APIs with different interfaces
- Making legacy code work with new systems
- Unifying multiple data sources or services
- Creating consistent interfaces for inconsistent libraries
When to Use
- You need to use existing classes with incompatible interfaces
- You want to create a unified interface for multiple similar services
- You're integrating third-party libraries or legacy systems
- You want to isolate your code from external API changes
Benefits
- Interface Unification: Make incompatible APIs work with the same interface
- Complexity Isolation: Keep messy integration details in dedicated adapter classes
- Easy Extension: Add new providers without changing existing business logic
- Maintainability: Fix bugs in one place instead of 20+ scattered locations
- Testability: Mock adapters easily for unit testing