Last Updated: February 13, 2026
Have you ever added a new feature to your codebase… only to find yourself editing dozens of existing classes, introducing bugs in places you didn’t even touch before?
Or been afraid to change something because… well, it might break something else?
If so, your code is likely violating one of the most important principles of object-oriented design: the Open-Closed Principle (OCP).
This chapter explains what OCP really means, why modifying existing code to add new features is risky, how to design systems that welcome new behavior without touching old code, and the common traps developers fall into when applying this principle.
Imagine you're building the checkout feature of an e-commerce platform. Initially, you only have one payment method: Credit Card.
Your PaymentProcessor class might look something like this (simplified, of course):
And here is how you use it in your checkout service:
So far, so good. But then your client comes along and says, "Hey, we need to add PayPal payments too."
No big deal, right? You go back and modify your PaymentProcessor class to handle both:
Then you update your CheckoutService to pick the right method based on the payment type:
Now it works for two methods. But guess what happens when the client wants you to add UPI, Bitcoin, or Apple Pay?
Each time, you are cracking open the PaymentProcessor class. Each time, you are adding another else if branch to CheckoutService. And each modification carries real risk.
Every time you modify an existing class to add new functionality, you expose yourself to several dangers.
1. Introducing Bugs. You might accidentally break the existing credit card or PayPal functionality while adding the new payment method. A misplaced brace, a wrong variable name, a copy-paste error. These things happen, and they happen more often in large, multi-branch classes.
2. Increased Testing Overhead. Every time you change the class, you need to re-test all of its functionality, not just the new part. The credit card processing still works, right? The PayPal flow still handles refunds correctly? You have to verify everything again.
3. Reduced Readability. The class becomes a sprawling collection of if-else if statements or a switch case that is hard to navigate and understand. New team members will struggle to figure out where one payment method ends and another begins.
4. Scalability Issues. Adding new payment types becomes progressively more difficult and error-prone. With ten payment methods, the class is a nightmare to maintain.
This constant modification is a direct violation of the Open-Closed Principle.
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. — Bertrand Meyer
Let's break that down:
Sounds like a paradox, right? How can you add new features without changing existing code? The answer lies in abstraction.
By programming against interfaces rather than concrete implementations, you can introduce new behavior simply by creating new classes that implement the existing interface.
The following diagram shows what an OCP-compliant design looks like for our payment system. The PaymentProcessor depends on a PaymentMethod interface, and each concrete payment type implements that interface.
Adding a new payment method, like BitcoinPayment, means creating a new class. Nothing existing changes.
Notice how PaymentProcessor only knows about the PaymentMethod interface. It has no idea whether it is processing a credit card, PayPal, UPI, or Bitcoin transaction. All the concrete implementations can be swapped in freely.
Before we jump into the implementation, let's understand why this principle is worth the effort.
Now let's see how to achieve all of this in practice.
Let's revisit our PaymentProcessor and see how we can make it OCP-compliant. The key is to introduce an abstraction for the payment methods.
We create a PaymentMethod interface that defines a contract for all payment types. Every payment method must implement a processPayment method.
Now, for each payment type, we create a separate class that implements this interface. Each class is self-contained and knows only about its own payment logic.
PaymentProcessor to Use the AbstractionOur PaymentProcessor now depends on the PaymentMethod interface, not concrete implementations. It no longer needs to know the specifics of each payment type. There are no if-else branches, no switch statements, and no reason to change when new payment methods arrive.
The CheckoutService simply passes the payment method to the processor. It does not need to know which payment type it is handling, it just delegates.
Look at that. Now, if the client wants to add "Bitcoin Payments" or "Apple Pay," what do we do?
BitcoinPayment) that implements PaymentMethod.processPayment method.That is it. The PaymentProcessor class remains unchanged. It is closed for modification but open for extension through new classes implementing the PaymentMethod interface.
This approach is often achieved using design patterns like the Strategy Pattern (which we have essentially implemented here) or the Decorator Pattern. Inheritance is another common mechanism, but as we have seen in previous chapters, composition through interfaces tends to be more flexible.
While OCP is powerful, it's not always straightforward, and developers can stumble into a few traps:
Applying OCP everywhere, for every conceivable future change, can lead to overly complex designs and unnecessary abstractions. Don't abstract things that are unlikely to change. Apply OCP strategically where change is anticipated.
"Closed for modification" doesn't mean you can never change a class. If there's a bug in the existing code, you absolutely must fix it. OCP applies to extending behavior, not to bug fixing or refactoring for clarity.
Creating too many layers of abstraction can make the code harder to understand and debug. The goal is clarity and maintainability, not abstraction for abstraction's sake.
If you're applying OCP mechanically without understanding the underlying goals (maintainability, scalability), you might create a system that follows the letter of the law but not its spirit.
Identifying where your system is likely to change is crucial. If you create extension points in stable parts of your system and hardcode the volatile parts, OCP won't help much. This often comes with experience and good domain understanding.
No, OCP primarily applies to adding new features or behaviors. Bug fixes are an exception; if your code has a flaw, you should definitely modify it to correct the issue. The "closed for modification" part means you shouldn't have to alter existing, working code to introduce new functionality.
Not necessarily for every single class from day one. OCP is most beneficial in parts of your system that you anticipate will change or have variations. If a piece of code is very stable and unlikely to have new variations, forcing OCP might be an over-complication.
It's a judgment call based on requirements and experience. Think about areas like business rules, integrations with external services, or UI components that might have different themes.
It might seem so initially, but the long-term benefits in terms of reduced risk, easier maintenance, and clearer separation of concerns often outweigh the effort of creating a few extra classes. Modern IDEs make class creation and management very easy. The alternative is often a monolithic, tangled class that becomes a nightmare to manage.