When building software, developers frequently encounter situations where new functionality must be added to an existing system. If every small change requires rewriting of your existing code, the system quickly becomes hard to maintain. And this is where the Open-Closed Principle (OCP) saves you from the trouble.
The Open–Closed Principle says that you should be able to add new behavior to a system without changing existing, working code. When followed correctly, OCP helps you build systems that are easier to scale, safer to change, and far less prone to breaking with every new feature.
This article is part of our SOLID Principles blog series, where we break down each principle with practical examples and real-world scenarios. In the previous blog, we explored the Single Responsibility Principle (SRP). If you haven’t read it yet, it’s highly recommended to start there, as SRP lays the foundation for applying OCP effectively.
As we move forward in this series, you’ll notice how these principles are deeply interconnected. SRP helps you create focused, well-defined components, while OCP builds on that by showing how to extend those components without rewriting them. Together, they form the backbone of maintainable and scalable system design.
Let’s now dive deeper and understand what the Open-Closed Principle really means, why it matters, and how to apply it correctly in real-world software systems.
What is the Open-Closed Principle?
According to Bertrand Meyer, “Software entities such as classes, modules, and functions should be open for extension but closed for modification.” This means that once a piece of code has been written, tested, and is working correctly, we should not change its existing logic when adding new features. Instead, we should extend its behavior using new code.
The goal of OCP is to reduce the risk of introducing bugs when new functionalities are added. By avoiding changes to existing code, we protect already tested functionality while still allowing the system to grow.
Now, let’s understand this with an example. Imagine you are designing a notification system for an application. Initially, the system only supports Email notifications. Later, the requirements change, and you are asked to add SMS notifications, and in the future, possibly push notifications.
Without following OCP
In this scenario, if you don’t follow OCP, you might keep modifying the same notification class every time a new notification type is added. This means:
- Existing code is repeatedly changed
- The class grows more complex
- The risk of breaking existing notification logic increases
By Applying OCP
Instead of changing the original notification logic, you design the system so that:
- The core notification interface remains unchanged
- Each new notification type (Email, SMS, Push) is added as a new implementation
- Existing code continues to work without any modification
In this approach, the system is extended with new behavior rather than modified, which is exactly what the Open–Closed Principle promotes.
Why the Open–Closed Principle Matters at Scale?
As software systems grow in size and complexity, change becomes unavoidable. New features are added, business rules evolve, and integrations increase. Large codebases often involve multiple developers working on different parts of the system. When existing classes are frequently modified, it becomes difficult to predict the impact of a change. The Open–Closed Principle (OCP) helps address these challenges by encouraging designs where new functionality is added through extension rather than modification.
By keeping existing code closed for modification, OCP ensures that stable, well-tested components remain untouched. At the same time, being open for extension allows teams to introduce new behavior in a controlled and isolated manner. This design approach becomes especially important in long-lived applications where continuous change is expected.
Benefits of Applying the Open–Closed Principle
When OCP is applied correctly, it provides several long-term advantages:
- Reduced risk of bugs
Since existing code is not modified, the chances of breaking already tested functionality are significantly lower. - Improved maintainability
Code is easier to understand and manage because changes are introduced through new components rather than scattered modifications. - Better scalability
Systems designed with OCP can grow in functionality without becoming tightly coupled or overly complex. - Easier collaboration in teams
Multiple developers can work on extending the system simultaneously without interfering with existing logic.
Applying the Open–Closed Principle
Let’s continue with a realistic example. A system that calculates discounts for different customer types in an e-commerce application.
Without OCP (Violating the Principle)
In this approach, every time a new customer type is introduced, the existing code must be modified.
class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if (customerType.equals("REGULAR")) {
return amount * 0.05;
} else if (customerType.equals("PREMIUM")) {
return amount * 0.10;
} else if (customerType.equals("VIP")) {
return amount * 0.20;
}
return 0;
}
}Problems at scale:
- Each new customer type requires changing existing logic
- The class grows with more if-else conditions
- High risk of bugs when modifying tested code
- Difficult for multiple teams to work in parallel
With OCP (Following the Principle)
Now let’s redesign the system using abstraction, making it open for extension but closed for modification.
Step 1: Create an abstraction
interface DiscountStrategy {
double applyDiscount(double amount);
}Step 2: Implement concrete strategies
class RegularCustomerDiscount implements DiscountStrategy {
public double applyDiscount(double amount) {
return amount * 0.05;
}
}
class PremiumCustomerDiscount implements DiscountStrategy {
public double applyDiscount(double amount) {
return amount * 0.10;
}
}
class VipCustomerDiscount implements DiscountStrategy {
public double applyDiscount(double amount) {
return amount * 0.20;
}
}Step 3: Use the abstraction in the calculator
class DiscountCalculator {
private DiscountStrategy discountStrategy;
public DiscountCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public double calculate(double amount) {
return discountStrategy.applyDiscount(amount);
}
}Why This Works Well at Scale
- To add a new customer type, you create a new class implementing DiscountStrategy
- No existing code is modified
- Each discount rule is isolated and testable
- Teams can add features independently
- The system remains stable as it grows
When NOT to Apply the Open–Closed Principle
While the Open–Closed Principle is a powerful design guideline, it should not be applied blindly. Applying OCP in every situation can lead to unnecessary complexity, especially in small or stable systems. Understanding when not to apply OCP is just as important as knowing when to use it.
1. When the requirements are simple and unlikely to change
If the system has very limited functionality and there is little to no expectation of future change, introducing abstractions can be unnecessary. In such cases, simple conditional logic may be easier to read and maintain than multiple interfaces and classes. For instance, a one-time script or a small internal tool with fixed rules does not benefit much from OCP-based design.
2. During early prototyping or proof-of-concept stages
In the initial phase of development, the focus is often on validating ideas quickly. Applying OCP too early can slow down development and distract from understanding the actual problem. At this stage, clarity and speed matter more than extensibility. Once the requirements stabilize, OCP can be introduced, where it adds real value.
3. When future changes are purely speculative
OCP encourages extension, but designing for imaginary future requirements often leads to overengineering. Creating abstractions for changes that may never happen increases code complexity without real benefits. A good rule of thumb is to apply OCP when a change is expected, not just possible.
4. When abstractions reduce code readability
Abstractions should make code more flexible, not harder to understand. If applying OCP results in too many layers that confuse readers, it may be better to keep the design simple. Readable code is often more valuable than perfectly abstract code.
Conclusion: Designing for Change, Not Rewrites
The Open–Closed Principle teaches us a crucial lesson in software design that change is inevitable, but breaking existing code doesn’t have to be. By designing systems that are open for extension and closed for modification, developers can add new functionality while keeping stable, tested code untouched.
Throughout this article, we explored what OCP means, why it becomes essential at scale, how it can be applied using abstractions, and when it should not be used. These ideas highlight that OCP is not about adding layers of abstraction everywhere, but about making deliberate design choices that support safe and predictable evolution of software systems. Good software design is about making thoughtful trade-offs, not following principles mechanically.
In the next part of this SOLID Principles blog series, we’ll move on to the Liskov Substitution Principle (LSP). While OCP focuses on how systems are extended, LSP ensures that these extensions behave correctly when substituted for one another. Understanding LSP is essential to making OCP-based designs truly reliable.