Adapter Design Pattern in C#
1. What is Adapter Pattern?
Adapter is a Structural Design Pattern that allows two incompatible interfaces to work together.
Simple definition:
Adapter converts the interface of an existing class into the interface that the client expects.
Think about a physical power adapter:
Laptop Charger
↓
Different Plug
↓
Power Adapter
↓
Wall Socket
The adapter doesn't change the charger or wall socket. It simply makes them compatible.
The same idea applies in software.
Our Application
↓
Expected Interface
↓
Adapter
↓
Existing / Third-Party Service
2. Why Do We Need Adapter Pattern?
Suppose our application has this interface:
public interface IPaymentService
{
void Pay(decimal amount);
}
Our application expects every payment provider to support:
Pay(amount);
We already have:
public class PaymentService : IPaymentService
{
public void Pay(decimal amount)
{
Console.WriteLine($"Payment ₹{amount} processed.");
}
}
Everything is fine.
using System;
public interface IPaymentService
{
void Pay(decimal amount);
}
public class PaymentService : IPaymentService
{
public void Pay(decimal amount)
{
Console.WriteLine($"Payment of ₹{amount} processed successfully.");
}
}
public class Program
{
public static void Main()
{
IPaymentService paymentService = new PaymentService();
paymentService.Pay(1500);
}
}
Later, the company decides to integrate a third-party payment gateway.
Assume the third-party library provides:
public class ThirdPartyPaymentGateway
{
public void MakePayment(double amount)
{
Console.WriteLine(
$"Third-party payment ₹{amount} processed.");
}
}
Now we have a problem.
Our application expects:
Pay(decimal amount);
But the third-party library provides:
MakePayment(double amount);
They are incompatible.
Our Application
↓
Pay(decimal)
X
MakePayment(double)
↑
Third-Party Gateway
We don't want to modify all of our application code.
We also may not be able to modify the third-party library.
This is exactly where the Adapter Pattern is useful.
3. Solution Using Adapter
We create an adapter between our application and the third-party service.
Our Application
↓
IPaymentService
Pay(decimal)
↓
PaymentAdapter
↓
Convert decimal → double
↓
MakePayment(double)
↓
ThirdPartyPaymentGateway
The adapter acts as a translator.
4. Simple Scenario
We will use these classes:
IPaymentService
▲
│
PaymentAdapter
│
▼
ThirdPartyPaymentGateway
The application understands:
IPaymentService
The third-party service has:
ThirdPartyPaymentGateway
The adapter connects them.
5. Step 1 — Create the Target Interface
public interface IPaymentService
{
void Pay(decimal amount);
}
This is called the Target Interface.
Why "Target"?
Because this is the interface that our application expects to use.
Our client wants:
paymentService.Pay(1000);
It doesn't want to know anything about the third-party API.
6. Step 2 — Third-Party Service
Imagine this class comes from an external payment library:
public class ThirdPartyPaymentGateway
{
public void MakePayment(double amount)
{
Console.WriteLine(
$"Third-party payment ₹{amount} processed.");
}
}
Notice the method:
MakePayment(double amount)
Our application expects:
Pay(decimal amount)
So they don't match.
This third-party class is called the Adaptee.
7. What is Adaptee?
This terminology is important for interviews.
Adaptee is the existing class that has an incompatible interface and needs to be adapted.
In our example:
ThirdPartyPaymentGateway
is the Adaptee.
It already works.
We don't want to change it.
We simply want our application to communicate with it.
8. Step 3 — Create Adapter
Now we create:
public class PaymentAdapter : IPaymentService
{
private readonly ThirdPartyPaymentGateway _gateway;
public PaymentAdapter(
ThirdPartyPaymentGateway gateway)
{
_gateway = gateway;
}
public void Pay(decimal amount)
{
double convertedAmount = (double)amount;
_gateway.MakePayment(convertedAmount);
}
}
This is the most important class.
The adapter implements:
IPaymentService
which our application understands.
Internally it contains:
ThirdPartyPaymentGateway
which is the incompatible third-party service.
9. Step 4 — Program.cs
public class Program
{
public static void Main()
{
ThirdPartyPaymentGateway gateway =
new ThirdPartyPaymentGateway();
IPaymentService paymentService =
new PaymentAdapter(gateway);
paymentService.Pay(1500);
}
}
Output:
Third-party payment ₹1500 processed.
10. Full Simple Program
using System;
// Target Interface
public interface IPaymentService
{
void Pay(decimal amount);
}
// Third-party / Existing class
// Adaptee
public class ThirdPartyPaymentGateway
{
public void MakePayment(double amount)
{
Console.WriteLine(
$"Third-party payment ₹{amount} processed.");
}
}
// Adapter
public class PaymentAdapter : IPaymentService
{
private readonly ThirdPartyPaymentGateway _gateway;
public PaymentAdapter(
ThirdPartyPaymentGateway gateway)
{
_gateway = gateway;
}
public void Pay(decimal amount)
{
// Convert application's decimal
// into third-party double
double convertedAmount = (double)amount;
// Call third-party method
_gateway.MakePayment(convertedAmount);
}
}
// Client
public class Program
{
public static void Main()
{
// Existing third-party service
ThirdPartyPaymentGateway gateway =
new ThirdPartyPaymentGateway();
// Wrap third-party service with Adapter
IPaymentService paymentService =
new PaymentAdapter(gateway);
// Our application uses its normal interface
paymentService.Pay(1500);
}
}
11. Output
Third-party payment ₹1500 processed.
The client called:
paymentService.Pay(1500);
But internally the third-party method was:
_gateway.MakePayment(1500);
The Adapter translated between the two.
12. Detailed Explanation
Let's understand the execution step-by-step.
First:
ThirdPartyPaymentGateway gateway =
new ThirdPartyPaymentGateway();
Memory conceptually contains:
gateway
↓
ThirdPartyPaymentGateway
The third-party service knows:
MakePayment(double amount)
13. Create Adapter
Next:
IPaymentService paymentService =
new PaymentAdapter(gateway);
We pass the third-party service into:
PaymentAdapter
Constructor:
public PaymentAdapter(
ThirdPartyPaymentGateway gateway)
{
_gateway = gateway;
}
Now:
paymentService
↓
PaymentAdapter
│
│ _gateway
↓
ThirdPartyPaymentGateway
The adapter holds a reference to the third-party object.
14. Client Calls Pay()
Now:
paymentService.Pay(1500);
The client thinks only in terms of:
IPaymentService
It knows:
Pay(decimal amount)
It doesn't care that internally we are using:
ThirdPartyPaymentGateway
15. Adapter Receives the Request
The Adapter's method executes:
public void Pay(decimal amount)
{
double convertedAmount = (double)amount;
_gateway.MakePayment(convertedAmount);
}
First:
decimal
1500
is converted to:
double
1500
Then:
_gateway.MakePayment(convertedAmount);
calls the third-party API.
16. Complete Execution Flow
Client
│
│ Pay(1500)
↓
IPaymentService
│
↓
PaymentAdapter
│
│ decimal → double
│
│ MakePayment(1500)
↓
ThirdPartyPaymentGateway
│
↓
Payment Processed
This is the core idea of Adapter.
17. Main Components of Adapter Pattern
There are normally four important participants.
| Component | Our Example | Purpose |
|---|---|---|
| Client | Program |
Wants to make payment |
| Target | IPaymentService |
Interface client expects |
| Adapter | PaymentAdapter |
Converts one interface to another |
| Adaptee | ThirdPartyPaymentGateway |
Existing incompatible class |
Remember this terminology.
18. Target
The Target is:
IPaymentService
public interface IPaymentService
{
void Pay(decimal amount);
}
This represents:
What our application expects.
19. Adaptee
The Adaptee is:
ThirdPartyPaymentGateway
It represents:
Existing functionality we want to reuse.
But its interface doesn't match our application's interface.
20. Adapter
The Adapter:
PaymentAdapter
implements:
IPaymentService
and internally calls:
ThirdPartyPaymentGateway
So:
Expected Interface
↓
Adapter
↓
Existing Interface
21. Client
The client doesn't need to know the conversion details.
It simply does:
IPaymentService paymentService = ...;
paymentService.Pay(1500);
This is important.
The client should not need to do:
var gateway = new ThirdPartyPaymentGateway();
gateway.MakePayment((double)amount);
everywhere.
The conversion/integration logic belongs in the Adapter.
22. Without Adapter
Imagine several services need the third-party gateway.
You might write:
double amount =
Convert.ToDouble(order.Amount);
_gateway.MakePayment(amount);
in:
OrderService
PaymentService
SubscriptionService
RenewalService
InvoiceService
Now third-party-specific code is spread throughout the application.
OrderService ────────────┐
PaymentService ──────────┤
SubscriptionService ─────┼── ThirdPartyPaymentGateway
InvoiceService ──────────┤
RenewalService ──────────┘
If the third-party API changes:
MakePayment()
to something else, many classes might need modification.
23. With Adapter
Instead:
OrderService ────────────┐
PaymentService ──────────┤
SubscriptionService ─────┼── IPaymentService
InvoiceService ──────────┤
RenewalService ──────────┘
↓
PaymentAdapter
↓
ThirdPartyPaymentGateway
Third-party-specific integration logic is isolated in one place.
If the external library changes, ideally we modify:
PaymentAdapter
instead of changing the whole application.
24. Adapter Can Convert More Than Method Names
Adapter isn't only for:
Pay()
↓
MakePayment()
It can translate several incompatibilities.
Method name
Pay()
↓
MakePayment()
Data type
decimal
↓
double
Request model
Your application:
PaymentRequest
Third-party:
GatewayPaymentRequest
Adapter can map:
PaymentRequest
↓
Adapter
↓
GatewayPaymentRequest
Response model
Third-party:
GatewayResponse
Adapter can convert it into:
PaymentResult
So in real projects an adapter often performs:
Request mapping
Data conversion
Method translation
Response mapping
Exception translation
25. More Realistic Example
Suppose our application uses:
public class PaymentRequest
{
public decimal Amount { get; set; }
public string Currency { get; set; } = string.Empty;
}
Our interface:
public interface IPaymentService
{
void Pay(PaymentRequest request);
}
But third party expects:
public class GatewayRequest
{
public double Total { get; set; }
public string CurrencyCode { get; set; } = string.Empty;
}
Adapter can perform:
public void Pay(PaymentRequest request)
{
var gatewayRequest = new GatewayRequest
{
Total = (double)request.Amount,
CurrencyCode = request.Currency
};
_gateway.MakePayment(gatewayRequest);
}
This is much closer to how adapters are used in real systems.
26. Real-World ASP.NET Core Usage
Adapter is particularly useful when integrating external systems such as:
- Payment gateways
- SMS providers
- Email providers
- Cloud storage
- Legacy systems
- External REST APIs
- Third-party SDKs
- Authentication providers
For example:
ASP.NET Core Application
↓
ISmsService
↓
TwilioAdapter
↓
Twilio SDK
Your business code only knows:
ISmsService
It doesn't need to know Twilio-specific classes.
If the provider changes later:
Before
ISmsService
↓
TwilioAdapter
↓
Twilio
After
ISmsService
↓
AnotherSmsAdapter
↓
Another Provider
The business layer can remain largely unchanged.
27. Adapter with Dependency Injection
In ASP.NET Core, Adapter works naturally with DI.
For example:
builder.Services.AddScoped<ThirdPartyPaymentGateway>();
builder.Services.AddScoped<IPaymentService, PaymentAdapter>();
Then the controller doesn't know about the Adapter or third-party implementation.
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
private readonly IPaymentService _paymentService;
public PaymentController(
IPaymentService paymentService)
{
_paymentService = paymentService;
}
[HttpPost]
public IActionResult Pay(decimal amount)
{
_paymentService.Pay(amount);
return Ok("Payment successful");
}
}
ASP.NET Core DI resolves:
PaymentController
↓
IPaymentService
↓
PaymentAdapter
↓
ThirdPartyPaymentGateway
This is a very practical implementation of Adapter in an ASP.NET Core application.
28. Adapter vs Decorator
Since you just learned Decorator, this difference is important.
Adapter
Purpose: Make incompatible interfaces compatible.
Application
↓
IPaymentService
↓
Adapter
↓
Different Third-Party Interface
It changes/translates the interface.
Decorator
Purpose: Add additional behavior.
Controller
↓
LoggingDecorator
↓
PaymentService
Both normally expose the same interface.
For example:
IPaymentService
Decorator adds:
Logging
Caching
Timing
Validation
without changing the expected contract.
Easy difference
Adapter
↓
Make incompatible interfaces compatible.
Decorator
↓
Add behavior to an existing object.
29. Adapter vs Facade
Another common interview question.
Adapter
Makes incompatible interfaces compatible.
Client
↓
Adapter
↓
Incompatible Service
Facade
Provides a simpler interface over a complex subsystem.
Client
↓
Facade
↓
Service A
Service B
Service C
So:
Adapter = Compatibility
Facade = Simplicity
30. Advantages
1. Reuse Existing Classes
You can reuse an existing or third-party class even when its interface doesn't match your application.
2. No Need to Modify Third-Party Code
You don't change:
ThirdPartyPaymentGateway
Instead:
ThirdParty code
↑
Adapter
↑
Application
3. Loose Coupling
Business code depends on:
IPaymentService
rather than:
ThirdPartyPaymentGateway
This reduces direct dependency on external libraries.
4. Third-Party Logic Is Centralized
Conversion logic remains inside:
PaymentAdapter
instead of being scattered across controllers and services.
5. Easier Provider Replacement
Today:
IPaymentService
↓
ProviderAAdapter
Tomorrow:
IPaymentService
↓
ProviderBAdapter
The application's core business code can continue using:
IPaymentService
6. Easier Testing
Your business service can depend on:
IPaymentService
which can be mocked or replaced during unit testing.
7. Supports SOLID Principles
Especially:
Single Responsibility Principle
The Adapter is responsible for integration/conversion.
Open/Closed Principle
New adapters can be introduced without rewriting existing business logic.
Dependency Inversion Principle
High-level application code can depend on:
IPaymentService
instead of depending directly on a third-party implementation.
31. Disadvantages
Adapter does introduce additional classes.
Instead of:
Application
↓
Third Party
you now have:
Application
↓
Interface
↓
Adapter
↓
Third Party
For a tiny application where the interfaces already match, an Adapter may be unnecessary.
Also, if an adapter becomes responsible for too much business logic, it can become complicated. Its primary responsibility should remain translation/integration.
32. When Should We Use Adapter?
Use Adapter when:
Existing interface doesn't match your application
Our Interface
≠
Existing Interface
Integrating third-party libraries
For example:
Payment Gateway
SMS Provider
Email Provider
Cloud SDK
Integrating legacy systems
Modern Application
↓
Adapter
↓
Legacy System
Replacing external providers
Keep your business code independent from vendor-specific APIs.
33. Easy Way to Remember
Think of a mobile charger.
Phone Charger
↓
Plug doesn't fit
↓
Adapter
↓
Wall Socket
You don't:
- Modify the charger
- Modify the wall socket
You put an adapter between them.
Similarly:
Our Application
↓
Interface doesn't match
↓
Adapter
↓
Existing / Third-Party System
Key Points
- Adapter is a Structural Design Pattern.
- It allows incompatible interfaces to work together.
- The Target is the interface expected by the application.
- The Adaptee is the existing/third-party class with an incompatible interface.
- The Adapter converts/translates between Target and Adaptee.
- The Client works with the Target interface.
- Adapter can translate method names, parameters, data types, request models and response models.
- It is commonly used with third-party APIs, SDKs and legacy systems.
- It prevents third-party-specific code from spreading throughout the application.
- It improves loose coupling and testability.
- It works naturally with ASP.NET Core Dependency Injection.
- Adapter usually uses composition by holding the Adaptee internally.
- Adapter = compatibility.
- Decorator = additional behavior.
- Facade = simplified interface.
- Use Adapter when an existing service provides the functionality you need but its interface does not match what your application expects.
One-line interview answer
Use Adapter when integrating a third-party SDK, external API, or legacy component whose contract doesn't match my application's contract. I keep the vendor-specific models and conversion logic behind my own interface so the business layer doesn't directly depend on the vendor.