Strategy Pattern Real-World Example in C#: Complete Implementation
This article presents a complete real-world example of the Strategy pattern in C#: an e-commerce discount system that handles multiple discount types. This Strategy pattern real-world example in C# demonstrates how to implement the pattern in a production-ready application with proper error handling, testing, and maintainability.
The Strategy pattern real-world example in C# we'll build shows how different discount calculation algorithms can be encapsulated as strategies, allowing the pricing system to switch between discount types without modifying core logic. This example demonstrates practical implementation techniques you can apply in your own C# applications.
If you're new to the Strategy pattern, understanding the fundamentals is essential before implementing it in production systems.
Problem Statement: E-Commerce Discount System
Our Strategy pattern real-world example in C# addresses a common e-commerce problem: calculating discounts for orders. The system needs to support multiple discount types:
- Percentage discounts: "20% off your order"
- Fixed amount discounts: "$10 off your order"
- Buy-one-get-one (BOGO) discounts: "Buy one, get one free"
- Tiered discounts: Different percentages based on order total
- Seasonal discounts: Time-based discount calculations
Without the Strategy pattern, this would require complex conditional logic that becomes difficult to maintain. Our Strategy pattern real-world example in C# shows how to solve this elegantly.
Solution Architecture
Our Strategy pattern real-world example in C# uses the following architecture:
- IDiscountStrategy interface: Defines the contract for all discount strategies
- Concrete strategy classes: Implement specific discount calculation algorithms
- PriceCalculator context: Uses strategies to calculate final prices
- DiscountFactory: Creates appropriate strategies based on discount type
- Supporting classes: Order, DiscountResult, and validation logic
This architecture demonstrates how Strategy pattern real-world example in C# provides flexibility and maintainability.
Step 1: Define the Strategy Interface
The first step in our Strategy pattern real-world example in C# is defining the discount strategy interface:
/// <summary>
/// Strategy interface for discount calculations.
/// All discount strategies must implement this interface.
/// </summary>
public interface IDiscountStrategy
{
/// <summary>
/// Calculates the discount amount for the given order total.
/// </summary>
/// <param name="orderTotal">The original order total before discount</param>
/// <returns>The discount amount to be subtracted</returns>
decimal CalculateDiscount(decimal orderTotal);
/// <summary>
/// Gets a human-readable description of the discount.
/// </summary>
string GetDiscountDescription();
/// <summary>
/// Validates whether this discount can be applied to the given order total.
/// </summary>
/// <param name="orderTotal">The order total to validate</param>
/// <returns>True if discount can be applied, false otherwise</returns>
bool CanApply(decimal orderTotal);
}
This interface is the foundation of our Strategy pattern real-world example in C#. It defines what all discount strategies must do, not how they do it.
Step 2: Implement Concrete Strategies
Now we implement concrete strategies for our Strategy pattern real-world example in C#. Each strategy encapsulates a specific discount calculation algorithm.
Percentage Discount Strategy
/// <summary>
/// Calculates discount as a percentage of the order total.
/// Example: 20% off means 20% of the order total is discounted.
/// </summary>
public class PercentageDiscountStrategy : IDiscountStrategy
{
private readonly decimal _percentage;
public PercentageDiscountStrategy(decimal percentage)
{
if (percentage < 0 || percentage > 100)
{
throw new ArgumentOutOfRangeException(
nameof(percentage),
"Percentage must be between 0 and 100");
}
_percentage = percentage;
}
public decimal CalculateDiscount(decimal orderTotal)
{
if (orderTotal < 0)
{
throw new ArgumentException("Order total cannot be negative", nameof(orderTotal));
}
if (!CanApply(orderTotal))
{
return 0m;
}
return orderTotal * (_percentage / 100m);
}
public string GetDiscountDescription()
{
return $"{_percentage}% off";
}
public bool CanApply(decimal orderTotal)
{
return orderTotal > 0;
}
}
Fixed Amount Discount Strategy
/// <summary>
/// Calculates discount as a fixed amount subtracted from the order total.
/// Example: $10 off means $10 is subtracted from the order total.
/// </summary>
public class FixedAmountDiscountStrategy : IDiscountStrategy
{
private readonly decimal _fixedAmount;
private readonly decimal? _minimumOrderTotal;
public FixedAmountDiscountStrategy(decimal fixedAmount, decimal? minimumOrderTotal = null)
{
if (fixedAmount < 0)
{
throw new ArgumentException("Fixed amount cannot be negative", nameof(fixedAmount));
}
_fixedAmount = fixedAmount;
_minimumOrderTotal = minimumOrderTotal;
}
public decimal CalculateDiscount(decimal orderTotal)
{
if (orderTotal < 0)
{
throw new ArgumentException("Order total cannot be negative", nameof(orderTotal));
}
if (!CanApply(orderTotal))
{
return 0m;
}
// Don't discount more than the order total
return Math.Min(_fixedAmount, orderTotal);
}
public string GetDiscountDescription()
{
var minOrderText = _minimumOrderTotal.HasValue
? $" (minimum order: ${_minimumOrderTotal.Value:F2})"
: "";
return $"${_fixedAmount:F2} off{minOrderText}";
}
public bool CanApply(decimal orderTotal)
{
if (orderTotal <= 0)
{
return false;
}
if (_minimumOrderTotal.HasValue && orderTotal < _minimumOrderTotal.Value)
{
return false;
}
return true;
}
}
Buy-One-Get-One Discount Strategy
/// <summary>
/// Calculates discount for buy-one-get-one (BOGO) promotions.
/// Example: Buy one item at full price, get the second item free.
/// </summary>
public class BogoDiscountStrategy : IDiscountStrategy
{
private readonly decimal _itemPrice;
private readonly int _quantity;
public BogoDiscountStrategy(decimal itemPrice, int quantity)
{
if (itemPrice < 0)
{
throw new ArgumentException("Item price cannot be negative", nameof(itemPrice));
}
if (quantity < 2)
{
throw new ArgumentException("Quantity must be at least 2 for BOGO", nameof(quantity));
}
_itemPrice = itemPrice;
_quantity = quantity;
}
public decimal CalculateDiscount(decimal orderTotal)
{
if (!CanApply(orderTotal))
{
return 0m;
}
// Calculate how many free items the customer gets
int freeItems = _quantity / 2;
return freeItems * _itemPrice;
}
public string GetDiscountDescription()
{
return $"Buy {_quantity / 2}, Get {_quantity / 2} Free";
}
public bool CanApply(decimal orderTotal)
{
// BOGO applies if order contains at least the required quantity
return orderTotal >= (_itemPrice * _quantity);
}
}
Tiered Discount Strategy
/// <summary>
/// Calculates discount based on order total tiers.
/// Example: 10% off orders over $100, 20% off orders over $200.
/// </summary>
public class TieredDiscountStrategy : IDiscountStrategy
{
private readonly List<DiscountTier> _tiers;
public TieredDiscountStrategy(List<DiscountTier> tiers)
{
if (tiers == null || tiers.Count == 0)
{
throw new ArgumentException("Tiers cannot be null or empty", nameof(tiers));
}
_tiers = tiers.OrderByDescending(t => t.MinimumOrderTotal).ToList();
}
public decimal CalculateDiscount(decimal orderTotal)
{
if (!CanApply(orderTotal))
{
return 0m;
}
// Find the highest tier that applies
var applicableTier = _tiers.FirstOrDefault(t => orderTotal >= t.MinimumOrderTotal);
if (applicableTier == null)
{
return 0m;
}
return orderTotal * (applicableTier.DiscountPercentage / 100m);
}
public string GetDiscountDescription()
{
var tierDescriptions = _tiers.Select(t =>
$"{t.DiscountPercentage}% off orders over ${t.MinimumOrderTotal:F2}");
return string.Join(", ", tierDescriptions);
}
public bool CanApply(decimal orderTotal)
{
return orderTotal > 0 && _tiers.Any(t => orderTotal >= t.MinimumOrderTotal);
}
}
public class DiscountTier
{
public decimal MinimumOrderTotal { get; set; }
public decimal DiscountPercentage { get; set; }
}
Step 3: Implement the Context Class
The context class in our Strategy pattern real-world example in C# uses strategies to calculate final prices:
/// <summary>
/// Context class that uses discount strategies to calculate final order prices.
/// </summary>
public class PriceCalculator
{
private readonly IDiscountStrategy _discountStrategy;
private readonly ILogger<PriceCalculator> _logger;
public PriceCalculator(IDiscountStrategy discountStrategy, ILogger<PriceCalculator> logger = null)
{
_discountStrategy = discountStrategy ?? throw new ArgumentNullException(nameof(discountStrategy));
_logger = logger;
}
/// <summary>
/// Calculates the final price after applying the discount strategy.
/// </summary>
public PriceCalculationResult CalculateFinalPrice(decimal orderTotal)
{
if (orderTotal < 0)
{
throw new ArgumentException("Order total cannot be negative", nameof(orderTotal));
}
try
{
if (!_discountStrategy.CanApply(orderTotal))
{
_logger?.LogInformation(
"Discount {DiscountType} cannot be applied to order total ${OrderTotal}",
_discountStrategy.GetDiscountDescription(),
orderTotal);
return new PriceCalculationResult
{
OriginalPrice = orderTotal,
DiscountAmount = 0m,
FinalPrice = orderTotal,
DiscountDescription = "No discount applied",
IsDiscountApplied = false
};
}
var discountAmount = _discountStrategy.CalculateDiscount(orderTotal);
var finalPrice = Math.Max(0, orderTotal - discountAmount); // Ensure price doesn't go negative
_logger?.LogInformation(
"Applied discount {DiscountType}: ${DiscountAmount} off ${OrderTotal} = ${FinalPrice}",
_discountStrategy.GetDiscountDescription(),
discountAmount,
orderTotal,
finalPrice);
return new PriceCalculationResult
{
OriginalPrice = orderTotal,
DiscountAmount = discountAmount,
FinalPrice = finalPrice,
DiscountDescription = _discountStrategy.GetDiscountDescription(),
IsDiscountApplied = true
};
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error calculating discount for order total ${OrderTotal}", orderTotal);
throw;
}
}
}
public class PriceCalculationResult
{
public decimal OriginalPrice { get; set; }
public decimal DiscountAmount { get; set; }
public decimal FinalPrice { get; set; }
public string DiscountDescription { get; set; }
public bool IsDiscountApplied { get; set; }
}
Step 4: Create Strategy Factory
A factory helps manage strategy creation in our Strategy pattern real-world example in C#:
/// <summary>
/// Factory for creating discount strategies based on discount type and parameters.
/// </summary>
public class DiscountStrategyFactory
{
public IDiscountStrategy CreateStrategy(DiscountType discountType, Dictionary<string, object> parameters)
{
return discountType switch
{
DiscountType.Percentage => new PercentageDiscountStrategy(
Convert.ToDecimal(parameters["percentage"])),
DiscountType.FixedAmount => new FixedAmountDiscountStrategy(
Convert.ToDecimal(parameters["amount"]),
parameters.ContainsKey("minimumOrderTotal")
? Convert.ToDecimal(parameters["minimumOrderTotal"])
: null),
DiscountType.Bogo => new BogoDiscountStrategy(
Convert.ToDecimal(parameters["itemPrice"]),
Convert.ToInt32(parameters["quantity"])),
DiscountType.Tiered => new TieredDiscountStrategy(
(List<DiscountTier>)parameters["tiers"]),
_ => throw new ArgumentException($"Unknown discount type: {discountType}")
};
}
}
public enum DiscountType
{
Percentage,
FixedAmount,
Bogo,
Tiered
}
Step 5: Complete Usage Example
Here's how to use our Strategy pattern real-world example in C#:
class Program
{
static void Main(string[] args)
{
// Example 1: Percentage discount
var percentageStrategy = new PercentageDiscountStrategy(20m); // 20% off
var calculator1 = new PriceCalculator(percentageStrategy);
var result1 = calculator1.CalculateFinalPrice(100m);
Console.WriteLine($"Original: ${result1.OriginalPrice:F2}, " +
$"Discount: ${result1.DiscountAmount:F2}, " +
$"Final: ${result1.FinalPrice:F2}");
// Output: Original: $100.00, Discount: $20.00, Final: $80.00
// Example 2: Fixed amount discount with minimum order
var fixedStrategy = new FixedAmountDiscountStrategy(15m, minimumOrderTotal: 50m);
var calculator2 = new PriceCalculator(fixedStrategy);
var result2 = calculator2.CalculateFinalPrice(60m);
Console.WriteLine($"Original: ${result2.OriginalPrice:F2}, " +
$"Discount: ${result2.DiscountAmount:F2}, " +
$"Final: ${result2.FinalPrice:F2}");
// Output: Original: $60.00, Discount: $15.00, Final: $45.00
// Example 3: Tiered discount
var tiers = new List<DiscountTier>
{
new DiscountTier { MinimumOrderTotal = 100m, DiscountPercentage = 10m },
new DiscountTier { MinimumOrderTotal = 200m, DiscountPercentage = 20m },
new DiscountTier { MinimumOrderTotal = 500m, DiscountPercentage = 30m }
};
var tieredStrategy = new TieredDiscountStrategy(tiers);
var calculator3 = new PriceCalculator(tieredStrategy);
var result3 = calculator3.CalculateFinalPrice(250m);
Console.WriteLine($"Original: ${result3.OriginalPrice:F2}, " +
$"Discount: ${result3.DiscountAmount:F2}, " +
$"Final: ${result3.FinalPrice:F2}");
// Output: Original: $250.00, Discount: $50.00, Final: $200.00
// Example 4: Using factory
var factory = new DiscountStrategyFactory();
var strategy = factory.CreateStrategy(
DiscountType.Percentage,
new Dictionary<string, object> { ["percentage"] = 15m });
var calculator4 = new PriceCalculator(strategy);
var result4 = calculator4.CalculateFinalPrice(200m);
Console.WriteLine($"Original: ${result4.OriginalPrice:F2}, " +
$"Discount: ${result4.DiscountAmount:F2}, " +
$"Final: ${result4.FinalPrice:F2}");
// Output: Original: $200.00, Discount: $30.00, Final: $170.00
}
}
Testing the Implementation
Testing is crucial for our Strategy pattern real-world example in C#. Here are unit tests:
[TestClass]
public class DiscountStrategyTests
{
[TestMethod]
public void PercentageDiscountStrategy_CalculateDiscount_ReturnsCorrectAmount()
{
// Arrange
var strategy = new PercentageDiscountStrategy(20m);
// Act
var discount = strategy.CalculateDiscount(100m);
// Assert
Assert.AreEqual(20m, discount);
}
[TestMethod]
public void FixedAmountDiscountStrategy_WithMinimumOrder_AppliesCorrectly()
{
// Arrange
var strategy = new FixedAmountDiscountStrategy(10m, minimumOrderTotal: 50m);
// Act & Assert
Assert.IsFalse(strategy.CanApply(40m)); // Below minimum
Assert.IsTrue(strategy.CanApply(60m)); // Above minimum
Assert.AreEqual(10m, strategy.CalculateDiscount(60m));
}
[TestMethod]
public void PriceCalculator_WithPercentageStrategy_CalculatesCorrectFinalPrice()
{
// Arrange
var strategy = new PercentageDiscountStrategy(15m);
var calculator = new PriceCalculator(strategy);
// Act
var result = calculator.CalculateFinalPrice(100m);
// Assert
Assert.AreEqual(100m, result.OriginalPrice);
Assert.AreEqual(15m, result.DiscountAmount);
Assert.AreEqual(85m, result.FinalPrice);
Assert.IsTrue(result.IsDiscountApplied);
}
}
Integration with Dependency Injection
Our Strategy pattern real-world example in C# integrates with dependency injection:
// In Program.cs or Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Register strategies
services.AddTransient<IDiscountStrategy, PercentageDiscountStrategy>();
services.AddTransient<IDiscountStrategy, FixedAmountDiscountStrategy>();
// Register factory
services.AddSingleton<DiscountStrategyFactory>();
// Register context
services.AddScoped<PriceCalculator>();
}
Conclusion
This Strategy pattern real-world example in C# demonstrates a complete, production-ready implementation of an e-commerce discount system. The example shows how Strategy pattern provides flexibility, maintainability, and testability. By encapsulating each discount calculation algorithm in its own strategy class, we can easily add new discount types, modify existing ones, and test each strategy independently.
The key benefits demonstrated in this Strategy pattern real-world example in C# include: elimination of complex conditional logic, easy extension with new discount types, independent testing of strategies, and clean separation of concerns.
Frequently Asked Questions
How does this Strategy pattern real-world example in C# handle edge cases?
This Strategy pattern real-world example in C# handles edge cases through input validation, the CanApply method that checks if discounts can be applied, and ensuring final prices never go negative. Each strategy validates its own requirements before calculating discounts.
Can I add new discount types to this Strategy pattern real-world example in C#?
Yes, adding new discount types to this Strategy pattern real-world example in C# is straightforward: create a new class implementing IDiscountStrategy, add it to the factory if needed, and use it with the PriceCalculator. No existing code needs modification, demonstrating the Open/Closed Principle.
How do I test this Strategy pattern real-world example in C#?
Test this Strategy pattern real-world example in C# by testing each strategy independently with unit tests, testing the PriceCalculator context with mock strategies, and testing the factory's strategy creation logic. Dependency injection makes testing easier.
What are the performance implications of this Strategy pattern real-world example in C#?
This Strategy pattern real-world example in C# adds minimal performance overhead from interface method calls. The performance cost is negligible for discount calculations, and the flexibility benefits typically outweigh any small performance cost.
How does this Strategy pattern real-world example in C# compare to using if-else statements?
This Strategy pattern real-world example in C# eliminates complex if-else chains, makes it easy to add new discount types without modifying existing code, and allows independent testing of each discount algorithm. If-else statements would require modifying the calculator class for each new discount type.
Can I use this Strategy pattern real-world example in C# with async operations?
Yes, you can extend this Strategy pattern real-world example in C# for async operations by making strategy methods return Task<decimal> instead of decimal. The context class would then await strategy calls, making it suitable for I/O-bound discount calculations.
How do I handle discount validation in this Strategy pattern real-world example in C#?
This Strategy pattern real-world example in C# handles validation through the CanApply method in each strategy, input validation in strategy constructors and methods, and the context class checking CanApply before calculating discounts. This ensures discounts are only applied when appropriate.

