Strategy Pattern: turning 200 lines of if/else into clean code
Every new business rule in your app means one more if. The feature moves forward, but the code gets less clear and less stable.
It can quickly turn into a jungle, and on some projects the Strategy Pattern helped me put my code back in order. This pattern separates the different variants of a behavior without touching the main logic. Instead of adding an if, you create a strategy that encapsulates that behavior. The code that uses it never changes.
That's what makes it so useful in systems that evolve often: a payment processor with several modes, a calculation engine with several formulas, a notification service with several channels.
The real case: contractual PDFs in insurance
I'm using the Strategy Pattern right now on an insurance application. Each contract must generate a custom contractual PDF depending on the product and insurer: the templates and displayed data differ per product type. Rather than piling up conditions, a strategy automatically picks the right generation logic based on context.
1. The interface: the common contract
// Interface every strategy must implement
public interface IBulletinPdfStrategy
{
// Initializes the strategy with the quote data
Task Initialize(Guid quoteId);
// Builds the product-specific JSON request
Task<string> BuildRequest();
}
2. The Factory: one centralized switch
public class BulletinPdfStrategyFactory
{
private readonly IServiceProvider _serviceProvider;
public async Task<IBulletinPdfStrategy> GetStrategy(string type, Guid quoteId)
{
IBulletinPdfStrategy strategy;
switch (type)
{
case Codes.Products.CHO:
strategy = _serviceProvider.GetRequiredService<BulletinPdfCHOStrategy>();
break;
case Codes.Products.CBNO:
strategy = _serviceProvider.GetRequiredService<BulletinPdfCbnoStrategy>();
break;
// ... other products
default:
throw new ArgumentException($"Unsupported product: {type}");
}
await strategy.Initialize(quoteId);
return strategy;
}
}
One centralized switch replaces all the scattered if/else. The Factory initializes and returns a ready-to-use strategy.
3. The base class: shared logic
public abstract class BaseBulletinPdfStrategy : IBulletinPdfStrategy
{
// Shared dependencies (repositories, managers...)
protected readonly IPricingManager _pricingManager;
protected readonly IContractsRepository _contractsRepository;
// Shared data loaded during initialization
protected Contract _contract;
protected ContractPdfPricingDto _pricing;
public async Task Initialize(Guid quoteId)
{
_contract = await _contractsRepository.GetContractByQuoteId(quoteId);
var pricingDetails = await _pricingManager.CalculatePricing(_contract);
_pricing = pricingDetails.ToContractPricingDto();
}
// Abstract method: each strategy implements its own logic
public abstract Task<string> BuildRequest();
}
4. A concrete strategy
public class BulletinPdfCbnoStrategy : BaseBulletinPdfStrategy
{
// Dependencies specific to CBNO only
private readonly ICbnoRepository _cbnoRepository;
public override async Task<string> BuildRequest()
{
if (_contract == null)
throw new InvalidOperationException("Strategy not initialized");
var bulletinData = new SimulationCbnoDataDto
{
CoverageStartDate = _contract.EffectiveDate?.ToString("dd/MM/yyyy"),
Subscriber = await GetSubscriberInfoAsync(_contract.Id),
// ... other CBNO-specific data
};
return JsonSerializer.Serialize(bulletinData);
}
}
5. The main service: no more if/else
private async Task<Stream> GenerateContractPdfFromApi(Contract contract, Guid quoteId)
{
var code = contract.Product.Code;
// The factory automatically picks the right strategy
var strategy = await _bulletinPdfStrategyFactory.GetStrategy(code, quoteId);
var contractDataForApi = await strategy.BuildRequest();
var response = await _generatePdfAppService.GenerateBulletinPdf(code, contractDataForApi);
return await response.Content.ReadAsStreamAsync();
}
The code stays identical whatever the product. Adding a new product? Create a strategy and register it in the Factory. This code will never be modified again.
Takeaway: before, if/else everywhere in the business code. After, a Factory that decides and strategies that execute. Still, watch out for over-engineering: for 2 simple variants, an if/else is largely enough.
Note: the examples are simplified for clarity (renamed identifiers, constructors and dependencies omitted). The focus is on the pattern's structure.
This article comes from one of my LinkedIn posts (in French): join the discussion ↗