1. What Is It?
The Open/Closed Principle states that software entities (classes, modules, functions) should be open for extension, but closed for modification. Originally formulated by Bertrand Meyer (1988) and popularized by Robert C. Martin, it means you should be able to add new behavior to a system without changing the existing, tested code.
Without OCP, adding a new feature requires editing existing classes. Every edit is a risk: you may introduce bugs, break existing tests, or require a new round of regression testing. OCP drives you toward a design where new behavior is added by writing new code — new subclasses, new implementations, new decorators — rather than cracking open existing files. The existing code becomes a stable foundation, not a target for modification.
A team maintains a payment processing service that currently handles credit card payments. They need to add support for PayPal. Which approach best aligns with the Open/Closed Principle?
2. How It Works
The core mechanic: identify what varies in your system and encapsulate that variation behind an . Callers depend on the ; new variants are added by implementing the abstraction, not by modifying the caller.
The classic example is a shape-area calculator. A naive implementation uses a series of if/elif checks on shape type — adding a new shape requires modifying the calculator. The OCP version defines a Shape abstraction; the calculator delegates to it, and new shapes are added without touching the calculator.
Mermaid Class Diagram
Python Example
from abc import ABC, abstractmethod
from typing import List
# BEFORE — OCP violation: calculator must be modified for every new shape
class AreaCalculatorBad:
def total_area(self, shapes: list) -> float:
total = 0.0
for shape in shapes:
if shape["type"] == "circle":
import math
total += math.pi * shape["radius"] ** 2
elif shape["type"] == "rectangle":
total += shape["width"] * shape["height"]
# Adding "triangle" requires modifying this method
return total
# AFTER — OCP applied: calculator is closed to modification, open to extension
class Shape(ABC):
"""Abstraction that makes the calculator closed for modification."""
@abstractmethod
def area(self) -> float:
...
class Circle(Shape):
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Triangle(Shape):
"""New shape added WITHOUT touching AreaCalculator."""
def __init__(self, base: float, height: float) -> None:
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
class AreaCalculator:
"""Closed for modification: never needs to change when new shapes are added."""
def total_area(self, shapes: List[Shape]) -> float:
return sum(shape.area() for shape in shapes)
# Usage
shapes: List[Shape] = [Circle(5), Rectangle(4, 3), Triangle(6, 8)]
calculator = AreaCalculator()
print(calculator.total_area(shapes)) # Works with any Shape subclassJava Example
import java.util.List;
// BEFORE — OCP violation (assumes older Circle/Rectangle classes with public fields)
public class AreaCalculatorBad {
public double totalArea(List<Object> shapes) {
double total = 0;
for (Object shape : shapes) {
if (shape instanceof Circle c) {
total += Math.PI * c.radius * c.radius;
} else if (shape instanceof Rectangle r) {
total += r.width * r.height;
}
// Must modify here to add Triangle
}
return total;
}
}
// AFTER — OCP applied
public interface Shape {
double area();
}
public class Circle implements Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
private final double width;
private final double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
// New shape added WITHOUT modifying AreaCalculator
public class Triangle implements Shape {
private final double base;
private final double height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
// Closed for modification
public class AreaCalculator {
public double totalArea(List<Shape> shapes) {
return shapes.stream()
.mapToDouble(Shape::area)
.sum();
}
}New shapes, unchanged calculator
The calculator calls area() on each Shape. Circle, Rectangle and the new Triangle supply their own area, so the calculator does not change.
The calculator accepts Circle, Rectangle, and Triangle through the same Shape abstraction.
Text version
- The calculator accepts Circle, Rectangle, and Triangle through the same Shape abstraction.
- Circle computes its area from its radius when the calculator calls the area method.
- Rectangle uses the same call but multiplies its own width and height.
- Triangle supplies its own area without adding a branch to the calculator.
- The calculator sums all returned areas, so adding Triangle leaves this caller unchanged.
A payment processing service currently handles two payment methods using if/elif checks: if method == 'credit_card' and elif method == 'paypal'. A new crypto payment method needs to be added. Which approach correctly applies the Open-Closed Principle?
3. Variants & Comparisons
OCP is achieved through different mechanisms depending on language and context:
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Inheritance / Abstract Class | Override behavior in subclasses | Natural for "is-a" hierarchies | Fragile base class problem; tight coupling | Well-defined taxonomies (shapes, animals) |
| Interface / Protocol | Implement new variant as new class | Loose coupling, testable | Requires upfront abstraction design | Most production OOP code |
| Strategy Pattern | Inject varying behavior as an object | Runtime-swappable | Slightly more indirection | Algorithms, pricing, sorting |
| Decorator Pattern | Wrap existing objects to add behavior | Composable, avoids subclass explosion | Ordering matters; harder to debug | Feature toggles, middleware, logging |
| Plugin / Configuration | Register new behavior via config | Zero code changes for new variants | Complex registration infrastructure | Frameworks, IDEs, CMS plugins |
Choose how behavior extends
Interfaces, strategies, and decorators support extension through different mechanisms, with different benefits and costs.
Interfaces accept new classes, strategies inject behavior objects, and decorators add behavior by wrapping objects.
Text version
- Interfaces accept new classes, strategies inject behavior objects, and decorators add behavior by wrapping objects.
- Interfaces loosen coupling, strategies allow runtime swaps, and decorators let behaviors compose.
- The costs differ: upfront interface design, added strategy indirection, and decorator ordering.
- Interfaces fit most production code, strategies fit algorithms like pricing, and decorators fit middleware and logging.
A team is building an e-commerce platform where discount calculations need to switch between different pricing algorithms at runtime — for example, switching from a flat-rate discount to a percentage-based one based on user tier. Which Open-Closed Principle mechanism is best suited for this requirement?
4. When to Use It (and When NOT To)
Use OCP when:
- A class has an
if/elifchain orswitchthat must grow every time a new variant is added - You have a core algorithm that different use cases must customize (pricing, validation, export format)
- You're building a library or framework where users must extend behavior without forking your code
Anti-patterns:
- Premature : Extracting an for behavior that only ever has one implementation. Wait for the second variant before abstracting.
- Violating OCP in the : Making the base class
area()method contain default logic with conditionals — the abstraction itself is now closed to extension. - Over-engineering with plugins: Building a plugin registry for behavior that changes once a year. OCP is a trade-off — the extension infrastructure has a cost.
Decision triggers:
- "If you see a growing
if shape_type == 'X'chain — reach for OCP via an abstraction." - "If adding a new feature means editing 5 existing files — your design is not closed for modification."
Use variation as the trigger
A growing shape-type branch chain is a signal to abstract; avoid extension infrastructure without a clear need.
Adding Triangle would grow the type checks because each shape has a different area formula.
Text version
- Adding Triangle would grow the type checks because each shape has a different area formula.
- Calling the area method on Shape keeps those formulas out of the calculator.
- New shapes implement that abstraction, so the calculator needs no new type check.
- With only one implementation, wait for the second variant before you add an abstraction.
A developer is building an internal admin tool where report exports are handled by a single function with an if/elif chain that checks the format type ('csv', 'pdf', 'excel'). A new format is requested once every 18 months, and each addition takes about 30 minutes. The developer proposes refactoring to a plugin registry with a base class and registered exporters. What is the most accurate assessment of this proposal?
5. Real-World Usage
Java's Comparator : Collections.sort() is closed for modification — it never changes. New sort orders are added by implementing Comparator<T>. The entire Java sort infrastructure follows OCP.
Django's middleware: Django's request/response pipeline is a chain of middleware objects. Adding new behavior (logging, authentication, rate limiting) means adding a new middleware class — the core WSGIHandler is never modified.
JUnit's Extension / TestRule: JUnit's test runner is closed for modification. New test lifecycle behaviors (test retries, custom logging, database setup) are added by implementing TestRule or an Extension, not by modifying the runner. An Extension is registered with @ExtendWith.
Your team wants to add rate limiting to a Django web application. Following the Open-Closed Principle, which approach best reflects how Django's request/response pipeline is designed to be extended?
6. Interview Cheat Sheet
Key sentences to say:
- "OCP means once a class is tested and shipped, I shouldn't need to crack it open to add new behavior — I extend it instead."
- "The mechanism is always the same: identify what varies, put it behind an , and add new variants by implementing the ."
- "The cost of OCP is the upfront abstraction. I apply it when I see the second use case — not the first."
- "OCP and the pattern are closely related — is the runtime mechanism that makes a class open for extension."
Common follow-up questions:
"Is OCP always achievable?"
No — some changes are inherently cross-cutting and require touching existing code. OCP is a design goal, not a law. The goal is to minimize modifications to stable, tested code.
"How do you decide when to abstract?"
The 'Rule of Three': on the first use case, implement it directly. On the second, consider abstracting. On the third, definitely abstract. Don't prematurely design for extensibility that may never be needed.
"What's the difference between OCP and just using interfaces?"
Interfaces are the mechanism; OCP is the principle. You can have interfaces everywhere and still violate OCP if you're constantly modifying the classes that depend on those interfaces.
Connections to other concepts:
- OCP is achieved mainly through — it lets you extend behavior without modifying callers
- Strategy Pattern is the most direct runtime implementation of OCP
- Pattern extends behavior by wrapping, which is OCP without
- Principle enables OCP — when high-level modules depend on abstractions, adding new low-level variants doesn't require touching the high-level code