Loading learning content...
Understanding the Adapter Pattern's mechanics is essential, but seeing it applied to real-world scenarios cements the knowledge. This page presents comprehensive use cases drawn from production systems, demonstrating how adapters solve actual engineering problems.
These examples span different domains — payment processing, logging, data access, and external API integration — showing the pattern's universal applicability.
By the end of this page, you will see the Adapter Pattern applied to payment gateway integration, logging framework abstraction, legacy system integration, and third-party API wrapping. Each example demonstrates practical considerations beyond the basic pattern.
E-commerce platforms must support multiple payment providers while maintaining a consistent internal interface. This is a classic adapter scenario.
The Challenge: Your application defines a PaymentProcessor interface, but Stripe, PayPal, and Square each have completely different SDKs with different data structures, error handling, and authentication mechanisms.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
// ═══════════════════════════════════════════════════════════// TARGET: Our domain's payment interface// ═══════════════════════════════════════════════════════════interface PaymentProcessor { charge(amount: Money, method: PaymentMethod): Promise<PaymentResult>; refund(transactionId: string, amount: Money): Promise<RefundResult>; getTransaction(id: string): Promise<Transaction | null>;} interface Money { amount: number; currency: string; }interface PaymentMethod { type: 'card' | 'bank'; token: string; }interface PaymentResult { success: boolean; transactionId: string; error?: string; } // ═══════════════════════════════════════════════════════════// STRIPE ADAPTER// ═══════════════════════════════════════════════════════════class StripeAdapter implements PaymentProcessor { constructor(private stripe: StripeSDK) {} async charge(amount: Money, method: PaymentMethod): Promise<PaymentResult> { try { const result = await this.stripe.paymentIntents.create({ amount: this.toCents(amount), currency: amount.currency.toLowerCase(), payment_method: method.token, confirm: true, }); return { success: true, transactionId: result.id }; } catch (error) { return { success: false, transactionId: '', error: this.mapError(error) }; } } async refund(transactionId: string, amount: Money): Promise<RefundResult> { const result = await this.stripe.refunds.create({ payment_intent: transactionId, amount: this.toCents(amount), }); return { success: result.status === 'succeeded', refundId: result.id }; } private toCents(money: Money): number { return Math.round(money.amount * 100); } private mapError(error: unknown): string { /* error translation */ return ''; }} // ═══════════════════════════════════════════════════════════// PAYPAL ADAPTER — Different SDK, same target interface// ═══════════════════════════════════════════════════════════class PayPalAdapter implements PaymentProcessor { constructor(private paypal: PayPalSDK) {} async charge(amount: Money, method: PaymentMethod): Promise<PaymentResult> { // PayPal uses different structure: orders, not payment intents const order = await this.paypal.orders.create({ purchase_units: [{ amount: { currency_code: amount.currency, value: amount.amount.toString() } }] }); const capture = await this.paypal.orders.capture(order.id); return { success: capture.status === 'COMPLETED', transactionId: order.id }; } // ... other methods adapted similarly} // ═══════════════════════════════════════════════════════════// CLIENT CODE — works with any payment processor// ═══════════════════════════════════════════════════════════class CheckoutService { constructor(private paymentProcessor: PaymentProcessor) {} async processOrder(order: Order): Promise<void> { const result = await this.paymentProcessor.charge( order.total, order.paymentMethod ); // Works identically regardless of Stripe, PayPal, or Square }}PaymentProcessor interface without real payment callsApplications should not be coupled to specific logging frameworks. Adapters provide logging abstraction, enabling framework changes without application code modification.
The Challenge: Different logging libraries (Winston, Pino, Bunyan, console) have different APIs, log levels, and output formats. Your application needs consistent logging regardless of the underlying implementation.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
// ═══════════════════════════════════════════════════════════// TARGET: Application's logging interface// ═══════════════════════════════════════════════════════════interface Logger { debug(message: string, context?: object): void; info(message: string, context?: object): void; warn(message: string, context?: object): void; error(message: string, error?: Error, context?: object): void;} // ═══════════════════════════════════════════════════════════// WINSTON ADAPTER// ═══════════════════════════════════════════════════════════class WinstonAdapter implements Logger { constructor(private winston: WinstonLogger) {} debug(message: string, context?: object): void { this.winston.debug(message, { metadata: context }); } info(message: string, context?: object): void { this.winston.info(message, { metadata: context }); } warn(message: string, context?: object): void { this.winston.warn(message, { metadata: context }); } error(message: string, error?: Error, context?: object): void { this.winston.error(message, { error: error?.stack, metadata: context }); }} // ═══════════════════════════════════════════════════════════// PINO ADAPTER — Structured JSON logging// ═══════════════════════════════════════════════════════════class PinoAdapter implements Logger { constructor(private pino: PinoLogger) {} debug(message: string, context?: object): void { this.pino.debug(context ?? {}, message); // Pino puts context first! } error(message: string, error?: Error, context?: object): void { this.pino.error({ ...context, err: error }, message); } // ... other methods} // ═══════════════════════════════════════════════════════════// CONSOLE ADAPTER — For development/testing// ═══════════════════════════════════════════════════════════class ConsoleAdapter implements Logger { debug(message: string, context?: object): void { console.debug(`[DEBUG] ${message}`, context ?? ''); } error(message: string, error?: Error, context?: object): void { console.error(`[ERROR] ${message}`, error, context ?? ''); } // ... other methods}This pattern is exactly what SLF4J does for Java logging. It provides a facade with adapters for Log4j, Logback, java.util.logging, etc. Your code uses SLF4J's interface; the adapter connects to whatever logging implementation is configured.
Modern applications often need to interact with legacy systems that use outdated protocols, data formats, or interaction patterns. Adapters bridge the technological gap.
The Challenge: A mainframe system exposes customer data through a fixed-width text protocol over TCP. Your modern microservices expect a CustomerRepository interface returning typed objects.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
// ═══════════════════════════════════════════════════════════// TARGET: Modern repository interface// ═══════════════════════════════════════════════════════════interface CustomerRepository { findById(id: string): Promise<Customer | null>; findByEmail(email: string): Promise<Customer | null>; save(customer: Customer): Promise<Customer>;} interface Customer { id: string; firstName: string; lastName: string; email: string; createdAt: Date;} // ═══════════════════════════════════════════════════════════// ADAPTEE: Legacy mainframe client// ═══════════════════════════════════════════════════════════class MainframeClient { async sendTransaction(code: string, data: string): Promise<string> { // Returns fixed-width text: "001JOHN DOE JOHN@EXAMPLE.COM " return ''; }} // ═══════════════════════════════════════════════════════════// ADAPTER: Bridges modern interface to legacy system// ═══════════════════════════════════════════════════════════class MainframeCustomerAdapter implements CustomerRepository { constructor(private mainframe: MainframeClient) {} async findById(id: string): Promise<Customer | null> { const paddedId = id.padStart(10, '0'); const response = await this.mainframe.sendTransaction('CUST_GET', paddedId); if (response.startsWith('ERR')) return null; return this.parseMainframeResponse(response); } async save(customer: Customer): Promise<Customer> { const mainframeData = this.formatForMainframe(customer); await this.mainframe.sendTransaction('CUST_UPD', mainframeData); return customer; } private parseMainframeResponse(response: string): Customer { // Parse fixed-width fields return { id: response.substring(0, 10).trim(), firstName: response.substring(10, 25).trim(), lastName: response.substring(25, 40).trim(), email: response.substring(40, 80).trim(), createdAt: this.parseMainframeDate(response.substring(80, 88)), }; } private formatForMainframe(customer: Customer): string { return [ customer.id.padStart(10, '0'), customer.firstName.padEnd(15), customer.lastName.padEnd(15), customer.email.padEnd(40), ].join(''); } private parseMainframeDate(dateStr: string): Date { // Mainframe uses YYYYMMDD format const year = parseInt(dateStr.substring(0, 4)); const month = parseInt(dateStr.substring(4, 6)) - 1; const day = parseInt(dateStr.substring(6, 8)); return new Date(year, month, day); }}This adapter handles multiple translation concerns:
The modern service layer works with clean Customer objects, completely unaware of the mainframe's antiquated interface.
Integrating external APIs (weather services, geocoding, email providers) requires adapters to isolate your domain from external data structures and API changes.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
// ═══════════════════════════════════════════════════════════// TARGET: Domain weather interface// ═══════════════════════════════════════════════════════════interface WeatherService { getCurrentWeather(location: Coordinates): Promise<Weather>; getForecast(location: Coordinates, days: number): Promise<Forecast[]>;} interface Weather { temperature: Temperature; conditions: string; humidity: number; windSpeed: number;} // ═══════════════════════════════════════════════════════════// OPENWEATHERMAP ADAPTER// ═══════════════════════════════════════════════════════════class OpenWeatherMapAdapter implements WeatherService { constructor( private apiKey: string, private httpClient: HttpClient ) {} async getCurrentWeather(location: Coordinates): Promise<Weather> { const response = await this.httpClient.get( `https://api.openweathermap.org/data/2.5/weather`, { lat: location.latitude, lon: location.longitude, appid: this.apiKey, units: 'metric', } ); return { temperature: { celsius: response.main.temp, feelsLike: response.main.feels_like, }, conditions: response.weather[0].description, humidity: response.main.humidity, windSpeed: response.wind.speed, }; } async getForecast(location: Coordinates, days: number): Promise<Forecast[]> { // Translate to OpenWeatherMap's forecast endpoint and parse response // ... }} // ═══════════════════════════════════════════════════════════// WEATHERAPI.COM ADAPTER — Different provider, same interface// ═══════════════════════════════════════════════════════════class WeatherApiAdapter implements WeatherService { async getCurrentWeather(location: Coordinates): Promise<Weather> { // WeatherAPI uses different endpoint structure and response format const response = await this.httpClient.get( `https://api.weatherapi.com/v1/current.json`, { q: `${location.latitude},${location.longitude}`, key: this.apiKey } ); return { temperature: { celsius: response.current.temp_c, feelsLike: response.current.feelslike_c, }, conditions: response.current.condition.text, humidity: response.current.humidity, windSpeed: response.current.wind_kph / 3.6, // Convert to m/s }; }}Across these use cases, several implementation patterns emerge that you'll apply repeatedly.
| Pattern | Description | When to Use |
|---|---|---|
| Type Mapping | Convert between domain types and external types | Always — core adapter responsibility |
| Error Translation | Map external errors to domain exceptions | When external errors need interpretation |
| Default Provision | Supply defaults for fields the adaptee requires but target doesn't provide | When interfaces have different required fields |
| Aggregation | Combine multiple adaptee calls into one target call | When target interface is coarser-grained |
| Caching | Cache adaptee responses to reduce external calls | When adaptee is slow or rate-limited |
| Retry Logic | Add resilience around adaptee calls | When adaptee is unreliable |
While adapters may include caching or retry logic, be careful not to overload them. If your adapter is handling complex business rules, it's becoming a service. Keep adapters focused on translation; extract other concerns into separate components.
You've mastered the Adapter Pattern! You understand why interface incompatibility is inevitable, how adapters solve it through wrapping, the difference between class and object adapters, and how the pattern applies in real-world scenarios. You're ready to apply this pattern confidently in your own systems.