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.
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
// 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)
// 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
// 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
// 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 lossInterview 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
IPaymentGatewayadapter wraps Stripe, yourIEmailSenderadapter wraps SendGrid, yourICacheServiceadapter 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?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.