7 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Open/Closed Principle (OCP)
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 subclass
Java Example
import java.util.List; // BEFORE — OCP violation 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(); } }
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 |
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."
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 @Extension, not by modifying the runner.
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 through — without , there's no way to 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
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.