Introduction
In software development, writing code is only half the job. The real challenge (and the real cost) comes later, when that code needs to change.
That's why maintainability is one of the most important qualities of a healthy codebase. Clean, modular code reduces bugs, accelerates onboarding, and supports business agility.
One pattern I find myself using frequently is the Strategy Pattern. It's a simple but powerful way to eliminate brittle if/else logic and replace it with clean, extensible design. In this post, I'll walk through a simple yet realistic example of different order pricing models and show how the Strategy Pattern helps us write code that's easier to understand, test, and evolve.
That's why maintainability is one of the most important qualities of a healthy codebase. Clean, modular code reduces bugs, accelerates onboarding, and supports business agility.
One pattern I find myself using frequently is the Strategy Pattern. It's a simple but powerful way to eliminate brittle if/else logic and replace it with clean, extensible design. In this post, I'll walk through a simple yet realistic example of different order pricing models and show how the Strategy Pattern helps us write code that's easier to understand, test, and evolve.
Setting the Stage: Dynamic Pricing Strategy
Let's say you're building a custom order system for a horticulture company. Different clients get different pricing strategies:
- Retail customers pay full price.
- Retail loyalty customers get a percentage discount.
- Wholesale customers get a volume discount based on order volume.
Before: The Classic If/Else Trap
public class PricingService(IWholesaleDiscountRepository wholesaleRepo,
LoyaltyProgramSettings loyaltyProgramSettings)
{
public decimal CalculatePrice(Customer customer, Order order)
{
if (customer.Type == CustomerType.Retail)
{
return order.BasePrice;
}
else if (customer.Type == CustomerType.Loyalty)
{
return order.BasePrice * (1 - loyaltyProgramSettings.DiscountRate);
}
else if (customer.Type == CustomerType.Wholesale)
{
var tiers = wholesaleRepo.GetDiscountTiers()
.OrderByDescending(t => t.MinQuantity);
var discount = tiers.FirstOrDefault(
t => order.Quantity >= t.MinQuantity)?.DiscountRate ?? 0m;
return order.BasePrice * (1 - discount);
}
throw new InvalidOperationException("Unknown customer type");
}
}
What's Wrong?
- The PricingService is doing too much with its branching logic and different calculations. Real-life scenarios will likely be much more complex too (like wholesale volume discounts are probably based on seasonal order volume, not a single order's volume).
- Depending on the complexity of your real-world scenario, you may want to apply the Open/Closed Principal, and that is impossible in this example.
- The logic is not modular - you can't reuse or test pricing strategies independently.
After: Strategy Pattern
Let's refactor this using the Strategy Pattern.
Step 1: Define the Strategy Interface
public interface IPricingStrategy
{
decimal CalculatePrice(Order order);
}
Step 2: Implement Strategies
public class RetailPricingStrategy : IPricingStrategy
{
public decimal CalculatePrice(Order order) => order.BasePrice;
}
public class LoyaltyPricingStrategy(LoyaltyProgramSettings loyaltyProgramSettings)
: IPricingStrategy
{
public decimal CalculatePrice(Order order)
=> order.BasePrice * (1 - loyaltyProgramSettings.DiscountRate);
}
public class WholesalePricingStrategy(IWholesaleDiscountRepository discountRepo)
: IPricingStrategy
{
public decimal CalculatePrice(Order order)
{
var tier = discountRepo.GetDiscountTiers()
.OrderByDescending(t => t.MinQuantity)
.FirstOrDefault(t => order.Quantity >= t.MinQuantity);
var discountRate = tier?.DiscountRate ?? 0m;
return order.BasePrice * (1 - discountRate);
}
}
Step 3: Strategy Factory
public interface IPricingStrategyFactory
{
IPricingStrategy GetStrategy(Customer customer);
}
public class PricingStrategyFactory(IServiceProvider provider)
: IPricingStrategyFactory
{
public IPricingStrategy GetStrategy(Customer customer) => customer.Type switch
{
CustomerType.Retail
=> provider.GetRequiredService(),
CustomerType.Wholesale
=> provider.GetRequiredService(),
CustomerType.Loyalty
=> provider.GetRequiredService(),
_ => throw new NotSupportedException("Unknown customer type")
};
}
Step 4: Application Code
public class OrderProcessor(IPricingStrategyFactory pricingStrategyFactory)
{
public void ProcessOrder(Customer customer, Order order)
{
var strategy = pricingStrategyFactory.GetStrategy(customer);
var price = strategy.CalculatePrice(order);
...
}
}
Conclusion
The Strategy Pattern isn't just a design tool. It's a way to build systems that are easier to understand, extend, and maintain.
In the example above, we started with a data-driven pricing model that worked, but was tightly coupled and procedural. By refactoring with the Strategy Pattern, we separated concerns and made each pricing rule modular.
This kind of architecture pays off in real-world scenarios:
These aren't just technical wins, they're business wins. Clean code reduces risk, accelerates delivery, and supports long-term agility.
In the example above, we started with a data-driven pricing model that worked, but was tightly coupled and procedural. By refactoring with the Strategy Pattern, we separated concerns and made each pricing rule modular.
This kind of architecture pays off in real-world scenarios:
- New pricing models can be added without touching existing logic.
- Each pricing model can be independently unit tested and verified easier.
- Business rules could potentially be loaded from configuration or external sources.
- Developers onboard faster and changes are easier/faster, because the code is easier to reason about.
These aren't just technical wins, they're business wins. Clean code reduces risk, accelerates delivery, and supports long-term agility.
About Latitude 40
Latitude 40 integrates experienced on-shore software development professionals into your organization, forming collaborative teams with or without your existing developers. Together, we identify needs, create tailored software solutions, and instill best practices that drive continuous improvement and ensure agility.
Need help modernizing your codebase or designing maintainable systems? Let's talk.
Need help modernizing your codebase or designing maintainable systems? Let's talk.
About the Author
Andrew Anderson is the President of Latitude 40 and a seasoned software architect with over two decades of experience in development and Agile delivery. He's worked globally as a developer, analyst, and coach, and is passionate about helping teams build software that is both powerful and maintainable.

RSS Feed