DEV Community

Abhinaw
Abhinaw

Posted on • Originally published at bytecrafted.dev

Stop Making Everything a Plugin: The Real Open/Closed Principle

The Open/Closed Principle (OCP) gets misunderstood a lot. It's not about turning every feature into a plugin system. It's about writing code that doesn't break when you add new stuff.

The Problem We're Solving

Here's what OCP violation looks like in real code:

public class PaymentProcessor
{
    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        switch (request.PaymentMethod)
        {
            case PaymentMethod.CreditCard:
                return await ProcessCreditCardAsync(request);
            case PaymentMethod.PayPal:
                return await ProcessPayPalAsync(request);
            // Every new payment method = modify this class
            default:
                throw new NotSupportedException();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Adding UPI Pay? You're modifying existing code. That's risky.

The Fix: Polymorphism Over Switch Statements

public interface IPaymentHandler
{
    PaymentMethod PaymentMethod { get; }
    Task<PaymentResult> ProcessAsync(PaymentRequest request);
}

public class PaymentProcessor(IEnumerable<IPaymentHandler> handlers)
{
    private readonly Dictionary<PaymentMethod, IPaymentHandler> _handlers = 
        handlers.ToDictionary(h => h.PaymentMethod);

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        if (!_handlers.TryGetValue(request.PaymentMethod, out var handler))
            throw new NotSupportedException();

        return await handler.ProcessAsync(request);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now adding UPI Pay is just:

public class UpiPayHandler : IPaymentHandler
{
    public PaymentMethod PaymentMethod => PaymentMethod.UpiPay;

    public async Task<PaymentResult> ProcessAsync(PaymentRequest request)
    {
        // UPI Pay logic here
        return new PaymentResult { Success = true };
    }
}
Enter fullscreen mode Exit fullscreen mode

Zero changes to existing code. That's OCP in action.

When to Actually Use This

Don't go crazy with abstractions. Use OCP when you have:

  • Business rules that change often (payment methods, tax calculations)
  • Multiple ways to do the same thing (different notification channels)
  • Algorithms you swap out based on conditions

Skip it for:

  • Simple data transformations
  • Configuration that rarely changes
  • One-off features

The Real Benefits

With proper OCP:

  • No regression risks - existing code stays untouched
  • Parallel development - teams can work on new handlers independently
  • Easy testing - each handler tests in isolation
  • Clean code - no more growing switch statements

Modern C# Makes This Easier

Using C# 12 primary constructors and dependency injection, this pattern is cleaner than ever. Register all your handlers automatically:

// In Program.cs
builder.Services.Scan(scan => scan
    .FromAssemblyOf<IPaymentHandler>()
    .AddClasses(classes => classes.AssignableTo<IPaymentHandler>())
    .AsImplementedInterfaces()
    .WithScopedLifetime());
Enter fullscreen mode Exit fullscreen mode

The Bottom Line

OCP isn't about making everything pluggable. It's about identifying the parts of your code that actually change and making those parts extensible.

Look for growing switch statements and repeated "modify existing code to add feature X" patterns. Those are your OCP opportunities.

The goal is simple: add new features by writing new code, not by changing old code.


This post is part of my SOLID Principles Series.

Related Posts in This Series


See also

Top comments (0)