Backend Systemsbeginner
Strategy Pattern in C#
Learn the Strategy pattern: define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. Practical C# examples with DI.
Asma HafeezApril 17, 20264 min read
csharpdesign-patternsstrategydotnetsolid
Strategy Pattern
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The client chooses which strategy to use at runtime.
The Problem Without Strategy
C#
// Hard-coded conditional logic — must edit this class to add a new payment method
public class PaymentService
{
public void Process(decimal amount, string method)
{
if (method == "credit")
{
// 50 lines of credit card logic
}
else if (method == "paypal")
{
// 50 lines of PayPal logic
}
else if (method == "crypto")
{
// 50 lines of crypto logic
}
// Adding Bitcoin requires modifying this class
}
}Strategy Pattern Solution
C#
// Strategy interface
public interface IPaymentStrategy
{
Task<PaymentResult> ProcessAsync(PaymentRequest request);
bool Supports(string method);
}
// Concrete strategies
public class CreditCardStrategy : IPaymentStrategy
{
public bool Supports(string method) => method == "credit";
public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
{
Console.WriteLine($"Processing credit card payment of {request.Amount:C}");
// Credit card specific logic
return new PaymentResult(true, "CC-" + Guid.NewGuid().ToString("N")[..8]);
}
}
public class PayPalStrategy : IPaymentStrategy
{
public bool Supports(string method) => method == "paypal";
public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
{
Console.WriteLine($"Processing PayPal payment of {request.Amount:C}");
return new PaymentResult(true, "PP-" + Guid.NewGuid().ToString("N")[..8]);
}
}
// Context — uses a strategy
public class PaymentService(IEnumerable<IPaymentStrategy> strategies)
{
public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
{
var strategy = strategies.FirstOrDefault(s => s.Supports(request.Method))
?? throw new NotSupportedException($"Payment method '{request.Method}' not supported.");
return await strategy.ProcessAsync(request);
}
}
// Records
public record PaymentRequest(decimal Amount, string Method, string Currency = "USD");
public record PaymentResult(bool Success, string TransactionId);Register Strategies in DI
C#
// Register all strategies — ASP.NET Core injects IEnumerable<IPaymentStrategy>
builder.Services.AddScoped<IPaymentStrategy, CreditCardStrategy>();
builder.Services.AddScoped<IPaymentStrategy, PayPalStrategy>();
builder.Services.AddScoped<IPaymentStrategy, CryptoStrategy>();
// Adding a new payment method: create a new strategy class, register it — done
// PaymentService requires zero changesStrategy with Factory
C#
public class PaymentStrategyFactory(IServiceProvider provider)
{
private readonly Dictionary<string, Type> _registry = new()
{
["credit"] = typeof(CreditCardStrategy),
["paypal"] = typeof(PayPalStrategy),
["crypto"] = typeof(CryptoStrategy),
};
public IPaymentStrategy GetStrategy(string method)
{
if (!_registry.TryGetValue(method.ToLower(), out var type))
throw new NotSupportedException($"Method '{method}' not supported");
return (IPaymentStrategy)provider.GetRequiredService(type);
}
}Real-World: Discount Strategy
C#
public interface IDiscountStrategy
{
decimal Apply(decimal price, Customer customer);
string Name { get; }
}
public class NoDiscount : IDiscountStrategy
{
public string Name => "None";
public decimal Apply(decimal price, Customer _) => price;
}
public class VipDiscount : IDiscountStrategy
{
public string Name => "VIP 20%";
public decimal Apply(decimal price, Customer customer)
=> customer.IsVip ? price * 0.8m : price;
}
public class SeasonalDiscount : IDiscountStrategy
{
private readonly decimal _percentage;
public string Name => $"Seasonal {_percentage}%";
public SeasonalDiscount(decimal percentage) => _percentage = percentage;
public decimal Apply(decimal price, Customer _) => price * (1 - _percentage / 100);
}
// Combine strategies
public class PricingEngine(IEnumerable<IDiscountStrategy> strategies)
{
public decimal Calculate(decimal basePrice, Customer customer)
{
return strategies.Aggregate(basePrice,
(price, strategy) => strategy.Apply(price, customer));
}
}Comparison: if/else vs Strategy
if/else approach:
+ Simple for 2-3 cases
- Each new case requires editing existing code (violates OCP)
- Hard to test each algorithm independently
- Grows unmanageable at 5+ cases
Strategy approach:
+ Each algorithm is a separate class — easy to test in isolation
+ Add new algorithms without changing existing code
+ DI registration makes adding strategies trivial
- More files for simple casesKey Takeaways
- Strategy replaces conditional logic with polymorphism — new behaviors are new classes
- Inject
IEnumerable<IStrategy>to get all registered strategies and pick dynamically - Strategies implement the Open/Closed Principle — open for extension, closed for modification
- Each strategy can be independently unit tested — no need to test the context with all strategies
- Good candidates: sorting algorithms, payment methods, shipping calculators, export formats
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.