Adapter Pattern Explained with Real Examples

Published: (December 7, 2025 at 07:12 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Introduction

You’re integrating a third‑party payment gateway into your application. Everything looks straightforward until you realize their SDK uses a completely different interface than what your codebase expects. Sound familiar? This is exactly where the Adapter Pattern shines.

The Adapter Pattern is one of the most practical design patterns from the Gang of Four. It acts as a bridge between two incompatible interfaces, allowing classes to work together that otherwise couldn’t. Think of it like a power adapter when you travel abroad—your device works the same way, but the adapter handles the incompatible outlet.

In this article, we’ll walk through the Adapter Pattern using real‑world .NET examples you’ll actually encounter in production code: payment gateways, logging systems, and legacy code integration.

What is the Adapter Pattern?

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

Key Components

  • Target Interface – The interface your client code expects to work with.
  • Adaptee – The existing class with an incompatible interface.
  • Adapter – The class that bridges the gap between Target and Adaptee.
  • Client – The code that uses the Target interface.

Real Example 1: Payment Gateway Integration

The Target Interface (What Your Application Expects)

// Your application's payment interface
public interface IPaymentProcessor
{
    Task ProcessPaymentAsync(
        decimal amount,
        string currency,
        string cardToken);
    Task RefundAsync(string transactionId, decimal amount);
}

public record PaymentResult(
    bool Success,
    string TransactionId,
    string? ErrorMessage);

public record RefundResult(
    bool Success,
    string RefundId,
    string? ErrorMessage);

The Adaptee (Stripe’s SDK – Simplified)

// Stripe's SDK has its own conventions
public class StripeClient
{
    public async Task CreateChargeAsync(StripeChargeRequest request)
    {
        // Simulate API call
        await Task.Delay(100);
        return new StripeCharge
        {
            Id = $"ch_{Guid.NewGuid():N}",
            Status = "succeeded",
            Amount = request.AmountInCents
        };
    }

    public async Task CreateRefundAsync(string chargeId, long amountInCents)
    {
        await Task.Delay(100);
        return new StripeRefund
        {
            Id = $"re_{Guid.NewGuid():N}",
            Status = "succeeded"
        };
    }
}

// Stripe uses cents, not decimal amounts!
public class StripeChargeRequest
{
    public long AmountInCents { get; set; }
    public string Currency { get; set; } = "usd";
    public string Source { get; set; } = string.Empty; // Card token
}

public class StripeCharge
{
    public string Id { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
    public long Amount { get; set; }
}

public class StripeRefund
{
    public string Id { get; set; } = string.Empty;
    public string Status { get; set; } = string.Empty;
}

The Adapter (Bridging the Gap)

public class StripePaymentAdapter : IPaymentProcessor
{
    private readonly StripeClient _stripeClient;
    private readonly ILogger _logger;

    public StripePaymentAdapter(
        StripeClient stripeClient,
        ILogger logger)
    {
        _stripeClient = stripeClient;
        _logger = logger;
    }

    public async Task ProcessPaymentAsync(
        decimal amount,
        string currency,
        string cardToken)
    {
        try
        {
            var request = new StripeChargeRequest
            {
                AmountInCents = (long)(amount * 100),
                Currency = currency.ToLowerInvariant(),
                Source = cardToken
            };

            var charge = await _stripeClient.CreateChargeAsync(request);

            return new PaymentResult(
                Success: charge.Status == "succeeded",
                TransactionId: charge.Id,
                ErrorMessage: null);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Stripe payment failed");
            return new PaymentResult(
                Success: false,
                TransactionId: string.Empty,
                ErrorMessage: ex.Message);
        }
    }

    public async Task RefundAsync(string transactionId, decimal amount)
    {
        try
        {
            var amountInCents = (long)(amount * 100);
            var refund = await _stripeClient.CreateRefundAsync(transactionId, amountInCents);

            return new RefundResult(
                Success: refund.Status == "succeeded",
                RefundId: refund.Id,
                ErrorMessage: null);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Stripe refund failed");
            return new RefundResult(false, string.Empty, ex.Message);
        }
    }
}

Real Example 2: Logging System Adapter

Target Interface

public interface IAppLogger
{
    void LogInfo(string message, params object[] args);
    void LogWarning(string message, params object[] args);
    void LogError(Exception ex, string message, params object[] args);
    void LogDebug(string message, params object[] args);
}

Adaptees (Different Logging Libraries)

// Serilog‑style logger
public class SerilogLogger
{
    public void Write(LogLevel level, string template, params object[] values) { }
    public void Write(LogLevel level, Exception ex, string template, params object[] values) { }
}

// Legacy logging library
public class LegacyLogger
{
    public void WriteToLog(string category, string message, int severity) { }
    public void WriteException(string category, Exception ex) { }
}

Adapters

public class SerilogAdapter : IAppLogger
{
    private readonly SerilogLogger _logger;

    public SerilogAdapter(SerilogLogger logger) => _logger = logger;

    public void LogInfo(string message, params object[] args)
        => _logger.Write(LogLevel.Information, message, args);

    public void LogWarning(string message, params object[] args)
        => _logger.Write(LogLevel.Warning, message, args);

    public void LogError(Exception ex, string message, params object[] args)
        => _logger.Write(LogLevel.Error, ex, message, args);

    public void LogDebug(string message, params object[] args)
        => _logger.Write(LogLevel.Debug, message, args);
}

public class LegacyLoggerAdapter : IAppLogger
{
    private readonly LegacyLogger _logger;
    private readonly string _category;

    public LegacyLoggerAdapter(LegacyLogger logger, string category = "Application")
    {
        _logger = logger;
        _category = category;
    }

    public void LogInfo(string message, params object[] args)
        => _logger.WriteToLog(_category, string.Format(message, args), severity: 1);

    public void LogWarning(string message, params object[] args)
        => _logger.WriteToLog(_category, string.Format(message, args), severity: 2);

    public void LogError(Exception ex, string message, params object[] args)
    {
        _logger.WriteToLog(_category, string.Format(message, args), severity: 3);
        _logger.WriteException(_category, ex);
    }

    public void LogDebug(string message, params object[] args)
        => _logger.WriteToLog(_category, string.Format(message, args), severity: 0);
}
Back to Blog

Related posts

Read more »