Open/Closed Principle in C#: Step-by-Step Guide with real-world Examples

In this article, I will break down the Open/Closed Principle in C# with step-by-step, real-world e-commerce examples. You’ll learn how to implement OCP to manage discounts and other evolving requirements, ensuring your application stays flexible, scalable, and maintainable over time.

The Open/Closed Principle (OCP), one of the five SOLID principles, provides a solution. OCP states that software entities should be “open for extension but closed for modification.” In practice, this means you can introduce new features by extending existing components rather than rewriting them, keeping your codebase clean, stable, and easy to scale.

Understanding the Open/Closed Principle in C#

What is the Open/Closed Principle (OCP)?

The Open/Closed Principle in C# states that classes should be open for extension but closed for modification. This means you can add new behavior without changing stable code. For example, instead of editing existing methods, you create new classes that extend functionality.

Why OCP Matters in Modern Software Development

In dynamic industries such as e-commerce, business requirements frequently change, with new discounts or shipping methods being introduced regularly. By following the Open/Closed Principle (OCP), your system can accommodate these changes by allowing new features to be added through extension, rather than by modifying existing, well-tested code. This approach helps maintain application stability and scalability, ensuring your software remains robust and adaptable as your business grows.
 

The Role of OCP in SOLID Principles

OCP is the second principle in SOLID and works alongside SRP and DIP to ensure clean architecture. It reduces technical debt, improves testability, and enhances maintainability. By applying OCP in C#, you make your codebase more resilient to change.

Real-World Challenges in E-Commerce Applications

E-commerce applications often need to adjust for seasonal sales, integrate new payment gateways, or add delivery providers. Without applying the Open/Closed Principle (OCP), each change forces modifications to existing code, raising risks and complexity.

In this article, we’ll explore a discount strategy example to show how challenging it becomes to manage growth when OCP is not followed. 

In this example, we applied a fixed discount. If we later need to add a percentage-based or seasonal discount, we would have to modify the existing code, resulting in extra testing, potential regressions, and an increased risk of introducing bugs.

public class DiscountService
{
    public decimal ApplyDiscount(string discountType, decimal totalAmount)
    {
        if (discountType == "Percentage")
        {
            return totalAmount - (totalAmount * 0.10m);
        }
        else if (discountType == "Fixed")
        {
            return totalAmount - 500;
        }
        else
        {
            return totalAmount;
        }
    }
}

Open/Closed Principle Explained with C# Examples

❌ OCP Violated – Bad Example in E-Commerce

  • In this example, the discount logic is directly hardcoded inside the checkout method.
  • Whenever we need a new discount type, we must change this same method.
  • This creates unnecessary dependency and makes the code harder to maintain.
  • Such an approach breaks the Open/Closed Principle (OCP).

Imagine you need to add two new types of discounts , one for seasonal offers and another for loyalty rewards for our valued customers. To achieve this, you’d add two new if conditions as shown below by extending the existing code with additional blocks.

public decimal ApplyDiscount(string type, decimal price, decimal rate, int loyaltyPoints)
{
    if (type == "Fixed") // Fixed discount
    {
        return price - 50;
    }
    if (type == "Percentage") // Percentage discount
    {
        return price - (price * 0.10m);
    }
    if(type == "Seasonal") // Seasonal discount
    {
         return price - (price * rate);
    }
    if(type == "Loyalty") // loyalty discount
    {
         return loyaltyPoints >= 1000 ? Math.Max(0, price * 0.95m) : price;
    }
    return price;
}
  • In the above example, we introduced two additional parameters to the method and added two new if blocks for handling extra discount types.
  • This approach makes the code tightly coupled, as any new discount type will require modifying the existing method.
  • Such changes increase the risk of breaking existing functionality and make unit testing more complex, since every change demands additional testing.
  • To overcome this, we should aim for a loosely coupled and scalable design that minimizes maintenance overhead in the long run.
  • This is where the Single Responsibility Principle (SRP) comes into play, helping us achieve cleaner, more maintainable, and extensible code.

Open/Closed Principle in E-Commerce: Step-by-Step Refactoring Guide

1. Create Interface
public interface IDiscountStrategy
{
    decimal ApplyDiscount(decimal price);
}
2. Implement in Classes
public class FixedDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal price) => price - 50;
}

public class PercentageDiscount : IDiscountStrategy
{
    public decimal ApplyDiscount(decimal price) => price - (price * 0.10m);
}

public sealed class SeasonalDiscount : IDiscountStrategy
{
    private readonly decimal _rate; // e.g., 0.15m
    public SeasonalDiscount(decimal rate = 0.15m) => _rate = rate;
    public decimal Apply(decimal price, OrderContext ctx) => ctx.IsSeasonal ? Math.Max(0, price - price * _rate) : price;
}

public sealed class LoyaltyDiscount : IDiscountStrategy
{
    public decimal Apply(decimal price, OrderContext ctx) => ctx.LoyaltyPoints >= 1000 ? Math.Max(0, price * 0.95m) : price;
}
3. Register in Startup
var builder = WebApplication.CreateBuilder(args);

// Register implementations with the interface
builder.Services.AddTransient<IDiscountStrategy, FixedDiscount>();
builder.Services.AddTransient<IDiscountStrategy, PercentageDiscount>();
builder.Services.AddTransient<IDiscountStrategy, SeasonalDiscount>();
builder.Services.AddTransient<IDiscountStrategy, LoyaltyDiscount>();
builder.Services.AddTransient<IDiscountStrategyFactory, DiscountStrategyFactory>();


var app = builder.Build();

app.Run();

4. Named/Keyed Strategy Pattern (Custom Factory)

public interface IDiscountStrategyFactory
{
    IDiscountStrategy GetStrategy(string type);
}

public class DiscountStrategyFactory : IDiscountStrategyFactory
{
    private readonly IEnumerable<IDiscountStrategy> _strategies;

    public DiscountStrategyFactory(IEnumerable<IDiscountStrategy> strategies)
    {
        _strategies = strategies;
    }

    public IDiscountStrategy GetStrategy(string type)
    {
        return type switch
        {
            "Fixed" => _strategies.OfType<FixedDiscount>().First(),
            "Percentage" => _strategies.OfType<PercentageDiscount>().First(),
            "Seasonal" => _strategies.OfType<SeasonalDiscount>().First(),
            "Loaylty" => _strategies.OfType<LoyaltyDiscount>().First(),
            _ => throw new ArgumentException("Invalid discount type")
        };
    }
}
5. Create a service and inject DiscountStrategyFactory
public class CheckoutService
{
    private readonly IDiscountStrategyFactory _factory;

    public CheckoutService(IDiscountStrategyFactory factory)
    {
        _factory = factory;
    }
cs
    public decimal GetFinalPrice(decimal price, string type)
    {
        var strategy = _factory.GetStrategy(type);
        return strategy.ApplyDiscount(price);
    }
}

Benefits of Following the Open/Closed Principle

Improved Maintainability and Scalability

The Open/Closed Principle allows new features to be introduced by adding new classes rather than modifying existing ones. This ensures a more adaptable and scalable system architecture.

Minimized Risk of Bugs and Faster Feature Delivery

By avoiding changes to existing code, the likelihood of introducing defects is significantly reduced. This leads to greater stability, smoother testing, and faster release cycles.

Cleaner Architecture and Long-Term Cost Savings

OCP promotes a modular, well-structured codebase that is easier to extend, maintain, and test. This reduces technical debt, enhances system reliability, and lowers long-term development costs.

Summary

The article explains the Open/Closed Principle (OCP) in C#. It shows how this principle keeps software open for extension but closed for modification. Using real-world e-commerce scenarios, it highlights drawbacks of tightly coupled code, such as adding new discount types using multiple if blocks. This approach increases complexity, risks, and testing effort.

With a step-by-step refactoring guide, the article shows how to apply the Strategy Pattern and Dependency Injection. Discount types (Fixed, Percentage, Seasonal, Loyalty) become independent classes. This ensures scalability, clean architecture, and less maintenance overhead.
Following OCP lets developers build flexible, stable, and extensible systems. It helps minimize bugs, speed up feature delivery, and save on long-term development costs.

Leave a comment