Learnixo
Back to blog
Backend Systemsbeginner

Adapter — Bridge Incompatible Interfaces

The Adapter pattern in C#: wrap an incompatible interface so it fits an expected contract. Class adapter vs object adapter, real-world use with legacy services and third-party libraries.

Asma Hafeez KhanMay 24, 20264 min read
csharpdesign-patternsadapterstructuraldotnet
Share:š•

Adapter — Bridge Incompatible Interfaces

The Adapter pattern converts the interface of a class into another interface that clients expect. It lets classes work together that otherwise couldn't due to incompatible interfaces.


The Problem

C#
// Your code expects this interface:
public interface IPaymentGateway
{
    Task<PaymentResult> ChargeAsync(string cardToken, decimal amount, string currency);
}

// But you must integrate a third-party library with a completely different API:
public class StripeClient   // from Stripe NuGet package — you can't change this
{
    public async Task<StripeCharge> CreateChargeAsync(StripeChargeOptions options) { /* ... */ }
}

// Without Adapter: your code is tightly coupled to Stripe — impossible to swap later.

Object Adapter (Preferred)

C#
// Adapter wraps the incompatible class and implements your interface
public class StripePaymentAdapter : IPaymentGateway
{
    private readonly StripeClient _stripe;

    public StripePaymentAdapter(StripeClient stripe)
        => _stripe = stripe;

    public async Task<PaymentResult> ChargeAsync(string cardToken, decimal amount, string currency)
    {
        // Translate from your interface to Stripe's API
        var options = new StripeChargeOptions
        {
            Amount      = (long)(amount * 100),   // Stripe uses pence/cents
            Currency    = currency.ToLower(),
            Source      = cardToken,
            Description = "SystemForge order",
        };

        try
        {
            var charge = await _stripe.CreateChargeAsync(options);
            return new PaymentResult(
                Success:       charge.Status == "succeeded",
                TransactionId: charge.Id,
                Message:       charge.FailureMessage
            );
        }
        catch (StripeException ex)
        {
            return new PaymentResult(
                Success:       false,
                TransactionId: null,
                Message:       ex.Message
            );
        }
    }
}

// Consuming code never knows it's talking to Stripe:
public class CheckoutService(IPaymentGateway payment)
{
    public async Task<bool> ProcessAsync(string cardToken, decimal amount)
    {
        var result = await payment.ChargeAsync(cardToken, amount, "GBP");
        return result.Success;
    }
}

// Wire up in DI:
builder.Services.AddSingleton<StripeClient>();
builder.Services.AddScoped<IPaymentGateway, StripePaymentAdapter>();

Legacy Code Adapter

C#
// Old XML-based service from 2008 — cannot be changed
public class LegacyReportingSystem
{
    public string GenerateXmlReport(int userId, string reportType, DateTime startDate, DateTime endDate)
    {
        // Returns XML string
        return $"<report type='{reportType}'>...</report>";
    }
}

// Modern interface your application uses:
public interface IReportGenerator
{
    Task<ReportDto> GenerateAsync(ReportRequest request);
}

public class LegacyReportAdapter : IReportGenerator
{
    private readonly LegacyReportingSystem _legacy;

    public LegacyReportAdapter(LegacyReportingSystem legacy)
        => _legacy = legacy;

    public Task<ReportDto> GenerateAsync(ReportRequest request)
    {
        // Adapt modern request to legacy parameters
        string xml = _legacy.GenerateXmlReport(
            request.UserId,
            request.Type.ToString(),
            request.Period.Start,
            request.Period.End
        );

        // Adapt legacy XML output to modern DTO
        var report = ParseXml(xml);
        return Task.FromResult(report);
    }

    private static ReportDto ParseXml(string xml)
    {
        // XML → DTO conversion
        return new ReportDto { Content = xml, Format = "xml" };
    }
}

Two-Way Adapter

C#
// Adapter that satisfies two different interfaces at once
public class UniversalLogAdapter : ILogger, Microsoft.Extensions.Logging.ILogger
{
    private readonly ILogger _internal;
    private readonly Microsoft.Extensions.Logging.ILogger _msLogger;

    // Implements both — useful when bridging two logging systems
}

When to Use

āœ“ Integrating third-party libraries you can't modify
āœ“ Wrapping legacy code with a modern interface
āœ“ Making multiple services with different APIs interchangeable
āœ“ Testing — adapter makes it easy to swap real for fake

āœ— When you can change the original class directly (no adapter needed)
āœ— When the interfaces differ so dramatically that adaptation causes data loss

Interview Answer

"The Adapter pattern wraps an incompatible class and exposes the interface your code expects — the classic use case is integrating a third-party library (Stripe, SendGrid) or legacy code you can't modify. Prefer the object adapter (composition) over the class adapter (inheritance): it's more flexible and works with sealed classes. In .NET, nearly every infrastructure integration is an adapter — your IPaymentGateway adapter wraps Stripe, your IEmailSender adapter wraps SendGrid, your ICacheService adapter wraps Redis. The key benefit is decoupling: consuming code depends only on the interface, so swapping the underlying provider requires changing only the adapter and its DI registration."

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.