Adding features shouldn’t feel like defusing a bomb. Here’s how the Open/Closed Principle (OCP) helps you extend your code without changing what already works.
You’ve probably done this before:
You needed to add a small feature so you end up opening a class and writing yet another if statement. A couple of weeks later, you had to add another one. And then another. And before you know it, the code that once felt clean turns into a mess of conditions.
That’s the kind of slow creep the Open/Closed Principle is meant to prevent.
(Side note: we explored the Single Responsibility Principle in the previous part of this series, worth checking out if you missed it!)
What is the Open/Closed Principle❓
The Open/Closed Principle (OCP) is one of the five core principles of SOLID design. It states:
Software should be open for extension but closed for modification.
In simpler terms: you should be able to add new behavior without changing the existing code.
That doesn’t mean your code never changes. It means the parts that already work shouldn’t have to be touched every time something new is added.
Let’s exapand on this a bit. Why we need to have our code open for extension but close for modification?
🤔 Why Apply the Open/Closed Principle?
Because code that changes too often becomes risky, not just technically, but mentally too. You start avoiding updates, afraid of breaking something that already works. I’ve experienced this first-hand!
Even a small if in the wrong place can create bugs or unexpected behavior. That fear of touching code slows everything down.
OCP helps you avoid that by encouraging you to add new behavior without touching old logic. It keeps the stable parts of your code safe and untouched.
This leads to cleaner structure, easier testing, and a codebase that grows without becoming fragile.
Let’s understand this with the help of a quick example.
📉 What Happens When You Don’t Apply OCP?
Let’s say you’re building a discount engine for an e-commerce platform. Initially, you just support a flat discount:
public class DiscountCalculator
{
public decimal ApplyDiscount(string customerType, decimal amount)
{
if (customerType == "Regular")
{
return amount * 0.95m; // 5% off
}
return amount;
}
}
Now marketing wants loyalty discounts. Then seasonal promos. Then discounts based on cart value, customer tier, or coupon codes. You start stacking conditions:
if (customerType == "Gold")
{
// 15% off
}
else if (amount > 1000)
{
// 10% off
}
else if (couponCode == "SUMMER2025")
{
// 20% off
}
Before you know it, logic starts overlapping. You need flags to apply or skip certain discounts. And the order of these conditions becomes critical. One bug, and you end up giving 3 discounts instead of 1.
All you wanted was a clean way to apply business rules. Instead, you built a tightly-coupled maze.
Let’s now solve this maze by the power of Open Closed Principle!
🛠️ How to Apply the Open Closed Principle?
You make your code extensible by introducing abstractions.
Instead of hardcoding every rule into the DiscountCalculator with if statements, we shift the responsibility of applying individual discounts to their own dedicated classes.
Each rule implements a shared interface, IDiscountRule, which defines when the rule applies and how it calculates the discount.
public interface IDiscountRule
{
bool IsMatch(Customer customer, Cart cart);
decimal Apply(decimal amount);
}
This means each rule is completely self-contained. It knows when it should run and how to apply its logic. The calculator doesn’t care about what the rules are, it just loops through them and applies what matches.
You want to give loyalty discount? Simple, add a new implementation for the IDiscountRule interface.
public class LoyaltyDiscount : IDiscountRule
{
public bool IsMatch(Customer customer, Cart cart) => customer.Type == "Gold";
public decimal Apply(decimal amount) => amount * 0.85m;
}
A discount for cart with high value? Add another implementation.
public class HighValueCartDiscount : IDiscountRule
{
public bool IsMatch(Customer customer, Cart cart) => cart.Total > 1000;
public decimal Apply(decimal amount) => amount * 0.9m;
}
Just like that you can introduce as many discount rules as you’d like. And the great thing is no one implementation affects another one. They are all separate.
The DiscountCalculator now becomes a simple orchestrator. It doesn’t contain any discount rules itself. It just loops through all available rules and applies them if they match.
public class DiscountCalculator
{
private readonly IEnumerable<IDiscountRule> _rules;
public DiscountCalculator(IEnumerable<IDiscountRule> rules)
{
_rules = rules;
}
public decimal ApplyDiscount(Customer customer, Cart cart)
{
var amount = cart.Total;
foreach (var rule in _rules)
{
if (rule.IsMatch(customer, cart))
{
amount = rule.Apply(amount);
}
}
return amount;
}
}
Want to support a new type of discount next month? Just create a new class that implements IDiscountRule, register it with your dependency injection container, and you’re done. You never touch the DiscountCalculator class again.
⏩ When Is It Okay to Skip OCP?
Now here a very important thing to understand. Not every situation calls for this level of abstraction.
If the code is unlikely to change or grow, applying OCP too early might just create unnecessary layers. Why solve a problem you don’t have yet? Keep things simple when they genuinely are.
But when your code deals with rules, workflows, or business logic that could evolve, that’s when OCP shines. It helps you balance simplicity today with flexibility tomorrow.
Remember, OCP is just one tool in your design toolbox. It’s not a silver bullet. Use it when it makes sense . Not every problem needs it, and sometimes the problem doesn’t even exist yet.
Whenever in doubt follow this 3 Questions Test I always ask myself before adding any layer of abstraction. It helps me avoid over-engineering solutions and might help you too!
💭 What about you ? where have you seen the Open/Closed Principle make the biggest impact in your own codebase? Or maybe where you wish it had been applied?
Next up: Liskov Substitution Principle when polymorphism quietly breaks your code.
Hi, I’m Bilal 👋, a .NET developer sharing what I learn about building software, thinking in systems, and writing clean, practical code.
If you’re exploring similar ideas, let’s connect and learn together.
