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 provider
RefundService.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 logic
ReportingService.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+ files
Common 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