Skip to content

Adapter Pattern

The Adapter pattern converts the interface of one class into another interface that clients expect. It lets classes work together that otherwise couldn’t because of incompatible interfaces.

A travel power adapter lets a US plug (Type A) work in a UK socket (Type G). The adapter doesn’t change either the plug or the socket — it translates between them.

  • Integrating third-party libraries with your existing interfaces
  • Using legacy code alongside new code
  • Making two independently developed systems work together

You have an interface your system uses, and a third-party SDK with a different interface:

// Your application's payment interface
public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(string customerId, decimal amount, string currency);
Task RefundAsync(string transactionId);
}
// Third-party Stripe SDK (incompatible interface)
public class StripeClient
{
public async Task<StripeCharge> CreateChargeAsync(StripeChargeOptions options) { ... }
public async Task<StripeRefund> CreateRefundAsync(string chargeId) { ... }
}
// Adapter — wraps Stripe and implements your interface
public class StripePaymentAdapter : IPaymentGateway
{
private readonly StripeClient _stripe;
public StripePaymentAdapter(StripeClient stripe)
{
_stripe = stripe;
}
public async Task<PaymentResult> ChargeAsync(string customerId, decimal amount, string currency)
{
var options = new StripeChargeOptions
{
CustomerId = customerId,
Amount = (long)(amount * 100), // Stripe uses cents
Currency = currency.ToLower(),
};
var charge = await _stripe.CreateChargeAsync(options);
return new PaymentResult
{
TransactionId = charge.Id,
Success = charge.Status == "succeeded",
Amount = charge.Amount / 100m,
};
}
public async Task RefundAsync(string transactionId)
{
await _stripe.CreateRefundAsync(transactionId);
}
}
// Registration
services.AddScoped<IPaymentGateway, StripePaymentAdapter>();
// Usage — business code only knows IPaymentGateway
public class OrderService
{
private readonly IPaymentGateway _payment;
public OrderService(IPaymentGateway payment) => _payment = payment;
public async Task CompleteOrderAsync(Order order)
{
var result = await _payment.ChargeAsync(order.CustomerId, order.Total, "GBP");
if (!result.Success) throw new PaymentFailedException();
}
}
// Your logging interface
interface Logger {
info(message: string, meta?: object): void;
error(message: string, error?: Error): void;
debug(message: string, meta?: object): void;
}
// Third-party Winston logger (different interface)
import winston from 'winston';
// Adapter
class WinstonAdapter implements Logger {
private logger: winston.Logger;
constructor() {
this.logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
}
info(message: string, meta?: object): void {
this.logger.info(message, meta);
}
error(message: string, error?: Error): void {
this.logger.error(message, { error: error?.message, stack: error?.stack });
}
debug(message: string, meta?: object): void {
this.logger.debug(message, meta);
}
}
// Swapping to a different logger (Pino) requires only changing the adapter
class PinoAdapter implements Logger {
private logger = pino();
info(message: string, meta?: object) { this.logger.info(meta, message); }
error(message: string, error?: Error) { this.logger.error({ err: error }, message); }
debug(message: string, meta?: object) { this.logger.debug(meta, message); }
}

Object Adapter (shown above) — wraps an instance via composition. Preferred because it doesn’t require inheritance.

Class Adapter — uses multiple inheritance (where supported) to inherit from both the target and adaptee:

class StripeAdapter extends StripeClient implements IPaymentGateway {
// override StripeClient methods to match IPaymentGateway
}

Object adapter is more flexible — you can adapt different instances, including subclasses.

  • Doesn’t modify the existing code (third-party library stays unchanged)
  • Single Responsibility: the adapter handles the translation
  • Open/Closed: add new adapters without changing existing code
  • Easy to swap implementations — change the adapter, not the business logic