Back to blog
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
Share:𝕏

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 changes

Strategy 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 cases

Key Takeaways

  1. Strategy replaces conditional logic with polymorphism — new behaviors are new classes
  2. Inject IEnumerable<IStrategy> to get all registered strategies and pick dynamically
  3. Strategies implement the Open/Closed Principle — open for extension, closed for modification
  4. Each strategy can be independently unit tested — no need to test the context with all strategies
  5. Good candidates: sorting algorithms, payment methods, shipping calculators, export formats

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.