Open/Closed Principle (OCP)
1. What is the Open/Closed Principle?
Open/Closed Principle (OCP) is the second principle of SOLID.
Software entities should be open for extension, but closed for modification.
In simple English:
We should be able to add new functionality without repeatedly changing existing, stable code.
Here, software entities can mean:
- Classes
- Methods
- Modules
- Components
2. What does "Open" and "Closed" mean?
This is the most important part to understand.
Open for Extension
We should be able to add new behavior.
For example, an application currently supports:
Email
SMS
Tomorrow, the customer asks for:
WhatsApp
We should be able to add WhatsApp support.
So:
Open = New functionality can be added.
Closed for Modification
Adding WhatsApp should ideally not require repeatedly editing the existing notification-processing logic.
So:
Closed = Existing stable code should not need modification for every new variant.
Therefore:
Open for Extension
+
Closed for Modification
=
Open/Closed Principle
3. Why do we need OCP?
Suppose we have an e-commerce application.
Customers can receive notifications through:
Email
SMS
We create a NotificationService containing conditions:
if (type == "email")
{
// Send Email
}
else if (type == "sms")
{
// Send SMS
}
Later, the business asks:
Add WhatsApp notification.
We modify the same class:
else if (type == "whatsapp")
{
// Send WhatsApp
}
Later:
Add Push Notification.
Again:
else if (type == "push")
{
// Send Push Notification
}
Later:
Add Teams notification.
Again we modify the class.
Our code starts growing:
NotificationService
│
├── if Email
├── else if SMS
├── else if WhatsApp
├── else if Push
├── else if Teams
└── ...
Every new notification type requires us to modify the same existing logic.
That is the problem OCP tries to solve.
4. Before OCP — Bad Design
Let's first create a simple program without OCP.
using System;
public class NotificationService
{
public void SendNotification(
string type,
string message)
{
if (type == "email")
{
Console.WriteLine(
$"Email sent: {message}");
}
else if (type == "sms")
{
Console.WriteLine(
$"SMS sent: {message}");
}
}
}
public class Program
{
public static void Main()
{
NotificationService service =
new NotificationService();
service.SendNotification(
"email",
"Order placed successfully");
service.SendNotification(
"sms",
"Order placed successfully");
}
}
Output
Email sent: Order placed successfully
SMS sent: Order placed successfully
The program works.
But there is a design problem.
5. What is the Problem?
Currently:
NotificationService
│
├── Email
│
└── SMS
Now the customer says:
We need WhatsApp.
We have to open NotificationService and change it:
else if (type == "whatsapp")
{
Console.WriteLine(
$"WhatsApp sent: {message}");
}
Now:
NotificationService
│
├── Email
├── SMS
└── WhatsApp
Later Push Notification:
NotificationService
│
├── Email
├── SMS
├── WhatsApp
└── Push
Every new type means:
New Requirement
↓
Open NotificationService
↓
Modify Existing Code
↓
Retest Existing Functionality
↓
Possible Regression Bug
This design is not very extensible.
6. Apply Open/Closed Principle
Instead of putting every notification type inside one conditional method, we create an abstraction:
INotification
Then each notification type provides its own implementation.
INotification
↑
┌────────────┼────────────┐
│ │ │
EmailNotification SmsNotification WhatsAppNotification
Now new notification behavior can be introduced as another implementation.
7. Full Simple Program Using OCP
using System;
// Abstraction
public interface INotification
{
void Send(string message);
}
// Email implementation
public class EmailNotification : INotification
{
public void Send(string message)
{
Console.WriteLine(
$"Email sent: {message}");
}
}
// SMS implementation
public class SmsNotification : INotification
{
public void Send(string message)
{
Console.WriteLine(
$"SMS sent: {message}");
}
}
// WhatsApp implementation
public class WhatsAppNotification : INotification
{
public void Send(string message)
{
Console.WriteLine(
$"WhatsApp sent: {message}");
}
}
// Notification service
public class NotificationService
{
public void SendNotification(
INotification notification,
string message)
{
notification.Send(message);
}
}
// Program
public class Program
{
public static void Main()
{
NotificationService service =
new NotificationService();
INotification email =
new EmailNotification();
service.SendNotification(
email,
"Order placed successfully");
INotification sms =
new SmsNotification();
service.SendNotification(
sms,
"Order placed successfully");
INotification whatsapp =
new WhatsAppNotification();
service.SendNotification(
whatsapp,
"Order placed successfully");
}
}
Output
Email sent: Order placed successfully
SMS sent: Order placed successfully
WhatsApp sent: Order placed successfully
8. Program Explanation
Let's understand each part.
Step 1 — Create the Abstraction
public interface INotification
{
void Send(string message);
}
INotification defines a contract.
It says:
Any notification implementation must provide a
Send()method.
It doesn't care how the notification is sent.
For example:
INotification
│
└── Send()
Different implementations can provide different behavior.
9. Email Notification
public class EmailNotification : INotification
{
public void Send(string message)
{
Console.WriteLine(
$"Email sent: {message}");
}
}
EmailNotification implements:
INotification
Its responsibility is to provide the email version of Send().
Conceptually:
INotification
↑
│
EmailNotification
│
↓
Send Email
10. SMS Notification
public class SmsNotification : INotification
{
public void Send(string message)
{
Console.WriteLine(
$"SMS sent: {message}");
}
}
The same interface is implemented by SmsNotification.
INotification
↑
│
SmsNotification
│
↓
Send SMS
11. WhatsApp Notification
public class WhatsAppNotification : INotification
{
public void Send(string message)
{
Console.WriteLine(
$"WhatsApp sent: {message}");
}
}
Again:
INotification
↑
│
WhatsAppNotification
│
↓
Send WhatsApp
12. NotificationService
Now look carefully at this class:
public class NotificationService
{
public void SendNotification(
INotification notification,
string message)
{
notification.Send(message);
}
}
This is the important part.
Notice that NotificationService doesn't contain:
if (type == "email")
or:
else if (type == "sms")
or:
else if (type == "whatsapp")
Instead, it works with:
INotification
Therefore it doesn't need to know whether the actual object is:
EmailNotification
SmsNotification
WhatsAppNotification
PushNotification
TeamsNotification
It simply says:
notification.Send(message);
The correct implementation executes through polymorphism.
13. How Does the Correct Method Execute?
Suppose:
INotification notification =
new EmailNotification();
Then:
notification.Send(message);
executes:
EmailNotification.Send()
If:
INotification notification =
new SmsNotification();
then the same:
notification.Send(message);
executes:
SmsNotification.Send()
If:
INotification notification =
new WhatsAppNotification();
then it executes:
WhatsAppNotification.Send()
This is polymorphism.
14. Program Flow
For Email:
Program
│
↓
EmailNotification
│
↓
NotificationService
│
↓
INotification.Send()
│
↓
EmailNotification.Send()
│
↓
Email Sent
For SMS:
Program
│
↓
SmsNotification
│
↓
NotificationService
│
↓
INotification.Send()
│
↓
SmsNotification.Send()
│
↓
SMS Sent
The NotificationService code stays the same.
15. Main Benefit — Adding a New Type
Now imagine that after six months the customer says:
We need Push Notification.
We create another implementation:
public class PushNotification : INotification
{
public void Send(string message)
{
Console.WriteLine(
$"Push notification sent: {message}");
}
}
That's the new behavior.
Then it can be used as:
INotification push =
new PushNotification();
service.SendNotification(
push,
"Order shipped successfully");
The important point is that we didn't need to add another condition inside:
NotificationService
The service continues to work with:
INotification
16. Why is this "Open for Extension"?
Originally:
INotification
↑
┌────┴─────┐
│ │
Email SMS
Later:
INotification
↑
┌────┼──────────┐
│ │ │
Email SMS WhatsApp
Later:
INotification
↑
┌────┼──────────┬──────┐
│ │ │ │
Email SMS WhatsApp Push
We can keep adding new implementations.
Therefore:
Open for extension
17. Why is this "Closed for Modification"?
The stable processing method remains:
public void SendNotification(
INotification notification,
string message)
{
notification.Send(message);
}
Whether we add:
Email
SMS
WhatsApp
Push
Teams
Slack
this method does not need a new if/else branch for each one.
Therefore, with respect to this expected variation:
Closed for modification
18. Before OCP vs After OCP
Before OCP
NotificationService
│
├── if Email
├── else if SMS
├── else if WhatsApp
└── else if Push
Adding a new notification type means modifying the conditional logic.
After OCP
INotification
↑
┌───────────────┼───────────────┐
│ │ │
Email SMS WhatsApp
Later:
INotification
↑
┌──────────┬────┼────┬──────────┐
│ │ │ │
Email SMS WhatsApp Push
New implementations extend the system.
19. OCP Doesn't Mean "Never Modify Existing Code"
This is an important interview point.
Some developers misunderstand OCP as:
Once a class is created, never modify it.
That is not correct.
Existing code can obviously be changed for:
- Bug fixes
- Security fixes
- Refactoring
- Changed business requirements
- Performance improvements
OCP mainly says:
Design the parts that are expected to vary so new variants can usually be added without repeatedly modifying stable logic.
20. Is Using an Interface Automatically OCP?
No.
Creating an interface alone doesn't automatically mean the design follows OCP.
For example, you could have:
public interface INotification
{
void Send();
}
but somewhere else still write:
if (type == "email")
{
// ...
}
else if (type == "sms")
{
// ...
}
else if (type == "whatsapp")
{
// ...
}
If every new implementation requires modifying important business logic throughout the system, the design may still have an OCP problem.
The goal is:
New variant
↓
Add implementation
↓
Existing stable processing logic
requires little/no change
21. Real-Time Industry Example
Consider a payment system.
Today:
Credit Card
UPI
Tomorrow:
PayPal
Later:
Apple Pay
A poor design could become:
PaymentService
│
├── if CreditCard
├── else if UPI
├── else if PayPal
├── else if ApplePay
└── ...
An OCP-oriented design could use:
IPayment
↑
┌──────────┼──────────┐
│ │ │
CreditCard UPI PayPal
Later:
IPayment
↑
┌─────────────┼─────────────┐
│ │ │
CreditCard UPI PayPal
│
+ ApplePay
The application is designed around the stable abstraction while individual payment behaviors are extensible.
22. OCP in ASP.NET Core
OCP is commonly seen in ASP.NET Core applications through:
- Interfaces
- Dependency Injection
- Polymorphism
- Strategy Pattern
- Factory Pattern
- Decorator Pattern
- Middleware
- Authentication handlers
- Logging providers
For example:
Controller
↓
IPaymentService
↑
│
CreditCardPaymentService
The controller doesn't need to know every technical detail of the implementation.
23. OCP and Dependency Injection
These concepts often work together.
Suppose:
OrderService
↓
INotification
↑
EmailNotification
ASP.NET Core DI can inject:
EmailNotification
as the implementation of:
INotification
Later, a different implementation can be supplied.
However:
Dependency Injection and OCP are not the same thing.
DI is a mechanism for supplying dependencies.
OCP is a design principle about extensibility and minimizing modification of stable code.
24. OCP and Design Patterns
Several design patterns commonly help implement OCP.
Strategy Pattern
Useful when different algorithms or behaviors can be selected.
Payment Strategy
├── CreditCard
├── UPI
└── PayPal
Factory Pattern
Useful for creating the appropriate implementation.
PaymentFactory
↓
IPayment
Decorator Pattern
Useful for adding behavior without modifying the original implementation.
Service
↓
Logging Decorator
↓
Caching Decorator
So SOLID principles and design patterns are closely related, but they are not identical.
25. Advantages of Open/Closed Principle
1. Easy to Extend
New functionality can often be introduced by adding a new class.
Existing:
Email
SMS
New:
WhatsApp
without rewriting the central processing logic.
2. Reduces Regression Risk
If stable code is modified less frequently, there is generally less chance of accidentally breaking existing behavior.
3. Better Maintainability
Instead of a huge conditional block:
if...
else if...
else if...
else if...
else if...
different behaviors are separated into dedicated implementations.
4. Better Testability
Each implementation can be tested independently:
EmailNotification
SmsNotification
WhatsAppNotification
5. Supports Polymorphism
Different implementations can be accessed through the same abstraction:
INotification
6. Supports Future Requirements
If new variants are expected, OCP can make those additions less disruptive.
7. Reduces Tight Coupling
Business logic doesn't need to know every concrete implementation.
Instead of:
NotificationService
↓
Email + SMS + WhatsApp + Push
we can have:
NotificationService
↓
INotification
26. When Should We Think About OCP?
OCP is especially useful when you notice code such as:
if (type == "A")
{
}
else if (type == "B")
{
}
else if (type == "C")
{
}
or:
switch (type)
{
case "A":
break;
case "B":
break;
case "C":
break;
}
A switch statement is not automatically bad.
But if the same conditional logic keeps changing whenever a new type is introduced, it may indicate an opportunity to apply:
- OCP
- Polymorphism
- Strategy Pattern
- Factory Pattern
27. Can OCP Be Overused?
Yes.
Suppose your application has one simple requirement:
Send Email
and there is no realistic expectation of multiple notification behaviors.
Creating:
INotification
NotificationBase
NotificationFactory
NotificationStrategy
EmailNotification
NotificationResolver
may create unnecessary complexity.
SOLID principles are design guidelines, not rules requiring maximum abstraction everywhere.
Apply OCP where variation and extension are reasonably expected.
28. Easy Way to Remember OCP
Remember:
Existing System
│
↓
Stable Code
│
Don't repeatedly
change it
│
↓
Add New Implementation
│
↓
New Functionality
Or simply:
Extend, don't repeatedly modify.
29. Key Points
- OCP = Open/Closed Principle.
- It is the O in SOLID.
- Software should be open for extension.
- Software should be closed for unnecessary/repeated modification.
- Open means new behavior can be added.
- Closed means stable existing logic should not require changes for every new variant.
- OCP does not mean existing code can never be changed.
- Interfaces, abstract classes, polymorphism and composition commonly help implement OCP.
- Dependency Injection can support an OCP-oriented design.
- Strategy, Factory and Decorator patterns often help achieve OCP.
- A growing
if/elseorswitchbased on behavior type can be a sign to consider OCP, though conditionals themselves are not violations. - OCP improves extensibility, maintainability, testability, and change isolation.
- Don't create unnecessary abstractions where extension is unlikely.
Interview Answer
Open/Closed Principle states that software entities should be open for extension but closed for modification. In simple terms, we should design the application so that new behavior can be added without repeatedly modifying existing stable code. This is commonly achieved using abstractions, polymorphism, composition, and dependency injection. For example, instead of modifying a notification service whenever Email, SMS, WhatsApp, or Push support is added, each notification type can implement a common interface and the existing processing logic can work with that abstraction.