The SOLID principles are a foundational group of guidelines for software design. They’re a bit like an apartment building. Each of the floors of the apartment is designed to support the next, providing stability, adaptability, and longevity. The term SOLID is an acronym for the first letter of each of the five principles:
S — Single Responsibility Principle
O — Open-Closed Principle
L — Liskov Substitution Principle
I — Interface Segregation Principle
D — Dependency Inversion Principle
As important as the SOLID principles are to software system design, these principles aren’t immune to criticism. The Open-Closed Principle (or OCP) is no exception to that debate.
The OCP isn’t just a theory. Reddit threads buzz with discussions about it, and Robert C. Martin’s (A.K.A. Uncle Bob, the inventor of the SOLID principles) blog posts passionately defend it. Many swear by it, while others warn against overuse.
This principle dictates that modules should be open to the extension of software entities such as modules, classes, and functions, but closed to the modification of these entities. Invariably, this allows for the character of a module to be extended while its source code remains unaltered.
Today, we’ll be exploring the Open-Closed Principle: from the criticisms around it, its best use cases, and common misapplication.
Let’s begin by answering the question: What does “open for extension, closed for modification” in OCP mean?
Imagine you have a box. That box is your application’s core. Over time, you want to add more compartments without opening or altering the box itself. So, what do you do instead? You just attach new sections. That’s extension. In code, you achieve this by adding new modules or classes that interact with the existing system without modifying it.
Modification, on the other hand, means tearing open the box and rearranging everything. That is dangerous. The original design might break, and new bugs can be introduced. OCP warns against this. It suggests that we structure our code so that new behavior can be plugged in without risking the old.
The phrase “open for extension” is very literal. It connotes a situation where a character in a module can be augmented or enhanced. This is simply achieved by adding new code, such as the inclusion of new subclasses or the implementation of new interfaces. Doing this allows the system to accommodate new features without any form of alteration to the existing code.
Once there is no alteration to the existing source code, it means that the module is closed to modification. Simply put, after a function has been tested and used, it should not be modified to include new functions.
At its core, OCP simply says, “Add new features by extending the code, not by changing what’s already there.”
Though OCP stands as a foundation for software design, it has sparked heated debates within the developer community. Critics argue that adhering strictly to the OCP can result in convoluted code structures, especially in cases of overuse or misapplication of the principle.
Some developers feel that the conventional application of the OCP (and other SOLID principles) heavily implies inheritance. This, they believe, can lead to tiny classes and one-line functions, which can result in a complicated codebase.
Uncle Bob has emphasized the importance of the OCP in the sustainability of clean software architecture. In his blog, he explained that designs that follow the OCP and the Dependency Inversion Principle (DIP) make isolation and separation easier, creating systems that are easier to maintain.
However, Uncle Bob also acknowledged that balancing these principles, especially when you have to integrate practices like Test-Driven Development (TDD), can be challenging.
When used right, OCP makes systems scalable and maintainable. When misapplied, it leads to endless abstraction layers that complicate code rather than simplifying it.
The idea behind the Open-Closed Principle didn’t actually originate from Robert C. Martin. It’s been around for over two decades, and has had two major interpretations over that time:
Bertrand Meyer is credited with founding the Open-Closed Principle, as used in his 1988 book, “Object-Oriented Software Construction.” In it, Meyer proposed that a module is open if it is available for extension, then new functions and fields can be added to it. Such a module is closed if it is available for use by other modules, indicating that it has a well-defined and stable interface.
In the 1990s, the open-closed principle was reinterpreted. This new interpretation emphasized the use of abstracted interfaces, leaving room for multiple implementations to be created and substituted for each other polymorphically. This became known as the Polymorphic Open-Closed Principle.
Contrary to Meyer’s definition, the Polymorphic interpretation supports inheritance from abstract base classes.
Today, the Open/Closed Principle (OCP) is widely understood to mean that software entities should be open for extension but closed for modification. In practice, this means you should add new functionality by extending existing code rather than changing it, helping preserve the original logic and reducing the risk of introducing bugs.
It’s important to note that changes in one part of the code can have ripple effects on other components. This can result in a rise in errors and unexpected issues. Also, a little tweak of the code can unravel hidden connections, compromising the system.
While the implementation of OCP streamlines the possibility of error infiltration, it’s worth noting that the extension of modules may not always be the best resort. Each new feature included via extension can require extra interfaces, classes, or chains of inheritance. This may end up bloating the code, and these extra layers of abstraction can increase memory utilization, thus slowing down execution. For performance-sensitive applications, this can become a real concern.
The application of the OCP has yielded many positive outcomes: clean software design, easily editable modules, and safer modification. However, there are a few instances where it can be considered harmful. So, when do you adhere to the OCP, and when should you stray away from it? Here are a couple of use cases to keep in mind.
Here are a few of the ways the OCP has been of tremendous help.
Large-scale systems are one of the biggest beneficiaries of the open-closed principle. When systems adhere to the principle of OCP, they have the luxury of scalability with a side of ease. When modules are designed in such a way that they can be extended without being modified, new features and components can easily be included without compromising the existing functionality. This allows for smoother integration of new features and parallel development.
Plugin-based architectures see huge benefits from the OCP, too. For these systems, the core application provides points that are extendable for plugins to modify functionalities. This design gives third-party developers a great basis to enhance applications while still maintaining the core of the codebase. In this case, OCP promotes not only flexibility but also customization.
For example, Integrated Development Environments (IDEs) like Visual Studio Code apply the OCP while using the plugin architectures. Developers can extend their modules to add features like debugging tools or language support. However, the main IDE remains unchanged. This way, the IDE is adaptable to solve the needs of varying developers without compromising its stability.
APIs can evolve when we apply the OCP. This way, new parameters and endpoints can be added without disrupting the functionality of existing clients. Designing APIs that follow the open-closed principle is crucial to maintaining backward compatibility while allowing for the introduction of new features.
For example, a web service can separate its endpoints into versions. The new versions introduce enhanced features while the existing version remains operational. With this in play, APIs can evolve to try new possibilities without their consumers feeling like immediate changes are being forced onto them, preserving stability and trust.
We’ve celebrated the different ways OCP helps. Now let’s evaluate some possible misapplications of the OCP.
Over-engineering happens when developers add too many abstractions to a system, most of which do not solve an existing need. The result is a complex codebase and system that’s difficult to understand and maintain.
For example, creating overly generic components in React codebases in anticipation of cases that might need these abstractions in the future can strain the ease of maintenance and understanding as the codebase becomes convoluted.
Interface explosion occurs when extra interfaces arise in a codebase due to the overzealous application of the open-closed principle. An example of this is highlighted in the .NET ecosystem, especially with the C# language. Developers sometimes design interfaces for every class to facilitate dependency injection and testing. As valuable as these interfaces are for defining contracts and promoting loose coupling, excessive use can clutter the codebase.
There are some ideas surrounding the open-closed principle and its applications that are inaccurate. This has further sponsored the misapplication of the Open-Closed Principle. So, let’s highlight these misconceptions and debunk them.
The open-to-extension and closed-to-modification idea of the OCP is often made to appear as a rule of thumb where, once code is written, there is zero tolerance for modification.
This is not a very accurate construct of the OCP. Yes, the principle indeed provides a premise for the extension of features, thereby placing a demand on the flexibility of a codebase to evolve into accepting new functionalities. And, yes, it is also true that this is to be designed in such a way that the existing codebase is unaltered. However, this doesn’t mean that modifying the code is forbidden. The idea is to minimize changes, not to put an eternal and immovable stop to changes.
The Single Responsibility Principle (SRP) emphasizes that a class should have only one reason for change. This means that each class should be saddled with a single responsibility. This is incredibly relevant to the application of the open-closed principle.
When focusing on extending code without modifying it, it’s important to apply the Single Responsibility Principle (SRP) alongside the Open/Closed Principle (OCP). Without SRP, you risk piling too much functionality into one class as you extend it, leading to messy, hard-to-maintain code. SRP helps by breaking your code into smaller, more focused classes — each with a single responsibility. Especially in large-scale systems, combining OCP with SRP ensures that your code stays modular, clean, and easier to scale.
The Dependency Inversion Principle (DIP) says that high-level and low-level modules shouldn’t depend directly on each other — they should depend on abstractions instead. In other words, the implementation details should be driven by abstract interfaces, not the other way around.
When paired with the Open/Closed Principle (OCP), DIP helps define how dependencies flow in your code. By using abstractions, you can cleanly separate responsibilities between different layers of your system, making it more modular and easier to maintain.
That said, it’s possible to apply OCP without fully following DIP. For example, a module might be designed to allow extensions (satisfying OCP), but still depend on concrete implementations rather than abstractions — a violation of DIP. So while applying DIP often leads to satisfying OCP, the reverse isn’t always true.
We’ve learned all about the OCP from its benefits to its misapplication. Now let’s put that knowledge to use and take a look at how we apply the OCP in different languages.
In Python, Abstract Base Classes (ABCs) make it possible for you to define common or generic bases for a group of related objects. With the use of these ABCs, you can make sure that new classes adhere to a specific interface. This way, extensions can be made without any form of modification to the existing code.
Here’s a simple example:
from abc import ABC, abstractmethod class Notification(ABC): @abstractmethod def send(self, message: str) -> None: pass class EmailNotification(Notification): def send(self, message: str) -> None: print(f"Sending email: {message}") class SMSNotification(Notification): def send(self, message: str) -> None: print(f"Sending SMS: {message}") def notify_user(notification: Notification, message: str): notification.send(message) if __name__ == "__main__": email = EmailNotification() sms = SMSNotification() notify_user(email, "Hello via Email!") notify_user(sms, "Hello via SMS!")
In this Python code, we define an abstract base class called Notification
. Two concrete classes, EmailNotification
and SMSNotification
, implement this interface. Notice that the notify_user
function works with the abstract Notification
type. We can extend this system by adding more types of notifications without touching the existing function. This is OCP in action.
Java is known for its robust type system. Strategy pattern combined with interfaces allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This complies with the OCP as you can introduce new features without altering the existing codebase.
Let’s see a Java example:
// Define an interface for notifications public interface Notification { void send(String message); } // Implement email notification public class EmailNotification implements Notification { @Override public void send(String message) { System.out.println("Sending email: " + message); } } public class SMSNotification implements Notification { @Override public void send(String message) { System.out.println("Sending SMS: " + message); } } public class NotificationService { public void notifyUser(Notification notification, String message) { notification.send(message); } public static void main(String[] args) { NotificationService service = new NotificationService(); Notification email = new EmailNotification(); Notification sms = new SMSNotification(); service.notifyUser(email, "Hello via Email!"); service.notifyUser(sms, "Hello via SMS!"); } }
This Java example mirrors the Python example. It defines a Notification
interface and concrete implementations. The NotificationService
class uses the interface to send messages. With this setup, if you want to add a new type of notification, you simply create a new class that implements Notification
. No existing code needs to change. The system remains robust and flexible.
TypeScript adds types to JavaScript. You can leverage higher-order components (HOCs) to extend features in TypeScript, especially within React applications. With this format, you can add functionality without modifying existing code. This adheres to the OCP by keeping the base component untouched while new functionality is added.
Here’s a TypeScript example:
import React from 'react'; interface ButtonProps { label: string; onClick: () => void; } class Button extends React.Component<ButtonProps> { render() { return ( <button onClick={this.props.onClick}> {this.props.label} </button> ); } } interface IconButtonProps extends ButtonProps { icon: string; } class IconButton extends Button { props: IconButtonProps; render() { return ( <button onClick={this.props.onClick}> <i className={`icon-${this.props.icon}`}></i> {this.props.label} </button> ); } } const App = () => { const handleClick = () => alert("Button clicked!"); return ( <div> <Button label="Click Me" onClick={handleClick} /> <IconButton label="Icon Click" icon="star" onClick={handleClick} /> </div> ); }; export default App;
In this example, the Button
component provides basic functionality. The IconButton
extends it, adding an icon. Note how the original Button
remains untouched. New behavior is added through extension, keeping with the OCP guidelines.
In C#, you can inject dependencies at runtime, enabling extensions without modification, and highlighting the application of the open-closed principle. C# embraces OCP by using interfaces and dependency injection (DI). DI frameworks help decouple components.
Let’s review a C# example:
using System; public interface INotification { void Send(string message); } public class EmailNotification : INotification { public void Send(string message) { Console.WriteLine("Sending email: " + message); } } public class SMSNotification : INotification { public void Send(string message) { Console.WriteLine("Sending SMS: " + message); } } public class NotificationService { private readonly INotification _notification; public NotificationService(INotification notification) { _notification = notification; } public void NotifyUser(string message) { _notification.Send(message); } } public class Program { public static void Main() { INotification email = new EmailNotification(); INotification sms = new SMSNotification(); NotificationService emailService = new NotificationService(email); NotificationService smsService = new NotificationService(sms); emailService.NotifyUser("Hello via Email!"); smsService.NotifyUser("Hello via SMS!"); } }
This C# snippet demonstrates how dependency injection works. The NotificationService
takes an INotification
in its constructor. This means you can pass in any implementation of the interface. The code remains untouched when you add new notification methods. This pattern is widely used in enterprise environments.
Applying the open-closed principle is not about automatically avoiding modification. It is more about being strategic about extension. The overall aim is to introduce change without destabilizing the system. This is only possible when the OCP is applied accurately. So, here are practices that will allow for the best application of the open-closed principle:
It can be tempting to apply OCP everywhere. But speculative design can be a trap. You can create an elaborate design with abstractions and endpoints that tend to over-deliver on the providence for future needs, leading to clutter.
Instead of doing that, streamline your focus to the real needs of the business, instead of blindly following a principle. If you’re considering using the OCP in your code, you can ask yourself the simple questions below to see if it’s really necessary:
You enable extension with ease when you interject dependencies rather than hand-coding them. This way, you can easily swap implementations without modifying existing code. This equally supports testability as mock dependencies are easy to interject.
Similarly, your codebase is likely to maintain its simplicity when you interplay the application of OCP and Interface Segregation Principle (ISP). This encourages the creation of small, focused interfaces rather than large all-in-one interfaces that can induce unwarranted multiple responsibilities in a class. Consequently, abstractions are minimized.
Not every extension needs a new class or interface. Sometimes, a straightforward refactor is the smarter, more readable solution. A few cases where a simple refactor is better than abstraction layers are:
The Open/Closed Principle is a cornerstone of writing flexible, maintainable code. By encouraging extension without modification, the OCP helps developers build systems that evolve safely over time. But like any principle, OCP isn’t a silver bullet. Overusing it can lead to overly complex or bloated designs.
The key is balance. Techniques like dependency injection, designing around real business needs (not just abstract principles), applying interface segregation, and refactoring with purpose all help keep OCP grounded in practical value.
Ultimately, think of OCP as a tool, not a rigid rule. Its goal isn’t to complicate your codebase, but to make it more adaptable and easier to scale. And sometimes, the smartest move is to favor simplicity. Use OCP where it makes sense, and let maintainability guide your decisions.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore how AI-driven testing tools like Shortest, Testim, Mabl, and Functionize are changing how we do end-to-end testing.
Examine the difference between profit vs. cost center organizations, and the pros and cons these bring for the engineering team.
Explore how to pass functions and structured objects as parameters in TypeScript, including use cases, syntax, and practical scenarios.
Why API documentation matters, recent trends in the space, and how to build great docs from scratch using Docusaurus, step by step.