๐งฉ Introduction
In modern application development, handling multiple payment methods like Credit Card and PayPal can quickly become a tangle of if-else
or switch
statements. This tight coupling makes your code harder to maintain and scale. Fortunately, the Strategy Pattern allows us to encapsulate these algorithms separately and choose them at runtime.
In this guide, you will learn how to implement the Strategy pattern in a clean and scalable way using C# with Dependency Injection (DI) support.
๐ฆ The Use Case: Payment Processing for Orders
Here is a basic Order
class that contains a payment method and a transaction ID that should be updated once the payment is processed:
public class Order
{
public int Id { get; set; }
public string? ProductName { get; set; }
public decimal Amount { get; set; }
public string? CustomerEmail { get; set; }
public PaymentMethod PaymentMethod { get; set; }
public string? Status { get; set; }
public string? PaymentTransactionId { get; set; }
}
We want to support two payment methods:
public enum PaymentMethod
{
CreditCard,
PayPal
}
๐ง Step 1: Define the Strategy Interface
Start by defining a shared interface for all payment strategies:
public interface IPaymentStrategy
{
string ProcessPayment(Order order);
}
๐ณ Step 2: Implement Concrete Payment Strategies
Credit Card Payment
public class CreditCardPaymentStrategy : IPaymentStrategy
{
public string ProcessPayment(Order order)
{
string transactionId = Guid.NewGuid().ToString();
Console.WriteLine($"Processing credit card payment for Order {order.Id}");
return transactionId;
}
}
PayPal Payment
public class PayPalPaymentStrategy : IPaymentStrategy
{
public string ProcessPayment(Order order)
{
string transactionId = Guid.NewGuid().ToString();
Console.WriteLine($"Processing PayPal payment for Order {order.Id}");
return transactionId;
}
}
๐งญ Step 3: Add a Strategy Resolver
Let the DI container manage the strategies. We'll use a dictionary to map them:
public class PaymentStrategyResolver
{
private readonly Dictionary<PaymentMethod, IPaymentStrategy> _strategies;
public PaymentStrategyResolver(IEnumerable<IPaymentStrategy> strategies)
{
_strategies = strategies.ToDictionary(
strategy => strategy switch
{
CreditCardPaymentStrategy => PaymentMethod.CreditCard,
PayPalPaymentStrategy => PaymentMethod.PayPal,
_ => throw new NotSupportedException("Unsupported payment method")
}
);
}
public IPaymentStrategy GetPaymentStrategy(PaymentMethod method)
{
if (_strategies.TryGetValue(method, out var strategy))
{
return strategy;
}
throw new NotSupportedException($"Payment method {method} is not supported.");
}
}
๐ง Step 4: Create the Order Processor
This service handles processing the order, updating the transaction ID and status:
public class OrderProcessor
{
private readonly PaymentStrategyResolver _resolver;
public OrderProcessor(PaymentStrategyResolver resolver)
{
_resolver = resolver;
}
public void Process(Order order)
{
var paymentStrategy = _resolver.GetPaymentStrategy(order.PaymentMethod);
order.PaymentTransactionId = paymentStrategy.ProcessPayment(order);
order.Status = "Paid";
Console.WriteLine($"Order {order.Id} completed. Status: {order.Status}, Transaction ID: {order.PaymentTransactionId}");
}
}
๐งฑ Step 5: Register Services in Dependency Injection
Register everything in Program.cs
(for ASP.NET Core or Console apps using generic host):
builder.Services.AddSingleton<IPaymentStrategy, CreditCardPaymentStrategy>();
builder.Services.AddSingleton<IPaymentStrategy, PayPalPaymentStrategy>();
builder.Services.AddSingleton<PaymentStrategyResolver>();
builder.Services.AddTransient<OrderProcessor>();
๐ Example Usage
var order = new Order
{
Id = 1,
ProductName = "Laptop",
Amount = 1200m,
PaymentMethod = PaymentMethod.CreditCard
};
var processor = serviceProvider.GetRequiredService<OrderProcessor>();
processor.Process(order);
Console.WriteLine($"Final Transaction ID: {order.PaymentTransactionId}");
โ Benefits of This Architecture
Extensible: Easily add Apple Pay or Crypto payment support by implementing a new
IPaymentStrategy
.Testable: Swap in mocks for unit testing without touching the core logic.
Decoupled:
OrderProcessor
knows nothing about how payments are processed.
๐งช Bonus: Persist to Database with EF Core
If you're using Entity Framework:
public class OrderProcessor
{
private readonly PaymentStrategyResolver _resolver;
private readonly ApplicationDbContext _dbContext;
public OrderProcessor(PaymentStrategyResolver resolver, ApplicationDbContext dbContext)
{
_resolver = resolver;
_dbContext = dbContext;
}
public void Process(Order order)
{
var paymentStrategy = _resolver.GetPaymentStrategy(order.PaymentMethod);
order.PaymentTransactionId = paymentStrategy.ProcessPayment(order);
order.Status = "Paid";
_dbContext.Orders.Update(order);
_dbContext.SaveChanges();
}
}
๐ Conclusion
The Strategy Pattern combined with Dependency Injection is a powerful pattern for building maintainable and flexible systems. By applying it to payment processing, you create a clean architecture that adheres to SOLID principles and scales as your application grows.
Github Repo: https://github.com/sekmenhuseyin/Payment-Strategy-Project