Skip to content

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The algorithm can vary independently from clients that use it.

  • Multiple algorithms for the same operation (sorting, compression, payment processing)
  • Selecting behaviour at runtime based on configuration or user choice
  • Replacing conditionals (if/switch) with polymorphism
  • Making behaviour easily testable by swapping in test doubles
// Strategy interface
public interface IDiscountStrategy
{
decimal Calculate(decimal orderTotal, Customer customer);
}
// Concrete strategies
public class NoDiscount : IDiscountStrategy
{
public decimal Calculate(decimal total, Customer customer) => 0;
}
public class LoyaltyDiscount : IDiscountStrategy
{
public decimal Calculate(decimal total, Customer customer)
{
return customer.LoyaltyPoints > 1000 ? total * 0.10m : total * 0.05m;
}
}
public class SeasonalDiscount : IDiscountStrategy
{
private readonly decimal _percentage;
public SeasonalDiscount(decimal percentage) => _percentage = percentage;
public decimal Calculate(decimal total, Customer customer) =>
total * (_percentage / 100);
}
public class BulkDiscount : IDiscountStrategy
{
public decimal Calculate(decimal total, Customer customer) =>
total > 500 ? total * 0.15m : 0;
}
// Context — uses the strategy
public class OrderPricer
{
private readonly IDiscountStrategy _discount;
public OrderPricer(IDiscountStrategy discount)
{
_discount = discount;
}
public decimal CalculateTotal(decimal subtotal, Customer customer)
{
var discount = _discount.Calculate(subtotal, customer);
return subtotal - discount;
}
}
// Usage — pick strategy at runtime
IDiscountStrategy strategy = customer.IsPremium
? new LoyaltyDiscount()
: new SeasonalDiscount(10);
var pricer = new OrderPricer(strategy);
var total = pricer.CalculateTotal(250.00m, customer);

In TypeScript, strategies can be plain functions rather than classes:

type SortStrategy<T> = (a: T, b: T) => number;
function sortBy<T>(items: T[], strategy: SortStrategy<T>): T[] {
return [...items].sort(strategy);
}
const products: Product[] = [...];
// Different strategies
const byPrice: SortStrategy<Product> = (a, b) => a.price - b.price;
const byName: SortStrategy<Product> = (a, b) => a.name.localeCompare(b.name);
const byRating: SortStrategy<Product> = (a, b) => b.rating - a.rating;
sortBy(products, byPrice); // cheapest first
sortBy(products, byName); // alphabetical
sortBy(products, byRating); // highest rated first

Before Strategy:

public decimal CalculateShipping(string method, decimal weight)
{
return method switch
{
"standard" => weight * 0.50m,
"express" => weight * 1.50m + 5,
"overnight" => weight * 3.00m + 15,
_ => throw new ArgumentException($"Unknown method: {method}")
};
}

After Strategy:

public interface IShippingStrategy
{
decimal Calculate(decimal weight);
}
public class StandardShipping : IShippingStrategy
{
public decimal Calculate(decimal weight) => weight * 0.50m;
}
public class ExpressShipping : IShippingStrategy
{
public decimal Calculate(decimal weight) => weight * 1.50m + 5;
}
// Add new methods without touching existing code — Open/Closed Principle
public class OvernightShipping : IShippingStrategy
{
public decimal Calculate(decimal weight) => weight * 3.00m + 15;
}
// Register strategies
services.AddKeyedScoped<IShippingStrategy, StandardShipping>("standard");
services.AddKeyedScoped<IShippingStrategy, ExpressShipping>("express");
services.AddKeyedScoped<IShippingStrategy, OvernightShipping>("overnight");
// Resolve by key
public class ShippingCalculator(
[FromKeyedServices("express")] IShippingStrategy strategy)
{
public decimal Calculate(decimal weight) => strategy.Calculate(weight);
}

The Strategy pattern is selected externally — the context doesn’t change strategy during its own lifecycle.

The State pattern transitions between states internally — the object changes its own behaviour based on its current state.