Chapter 02 · Article 04 of 55
Open-Closed Principle (OCP)
"Software entities (classes, modules, functions) should be open for extension, but closed for modification." - Bertrand Meyer, Object-Oriented Software Construction (1988)
Article outline14 sections on this page+
"Software entities (classes, modules, functions) should be open for extension, but closed for modification."
- Bertrand Meyer, Object-Oriented Software Construction (1988)
Overview
The Open-Closed Principle (OCP) is the second principle in the SOLID acronym and arguably the most architecturally significant. Bertrand Meyer introduced it in 1988, and Robert C. Martin later popularized it as a cornerstone of object-oriented design.
Open for extension means that the behavior of a module can be extended - you can add new functionality as requirements change.
Closed for modification means that extending behavior does not require changing the existing source code of that module.
At first glance this sounds contradictory. The resolution lies in abstraction: you define stable interfaces or abstract classes that act as fixed contracts, then introduce new behavior by creating new implementations rather than editing existing ones.
Meyer's original formulation relied on implementation inheritance. Martin's modern interpretation favors polymorphic OCP - depending on abstractions (interfaces/abstract classes) so that new concrete classes can be plugged in without touching consumer code.
The litmus test: Can you add a new feature by writing new code rather than changing existing code? If yes, your design respects OCP.
Problem It Solves
| Problem | Description |
|---|---|
| Fragile code | Modifying a working class to add a feature risks introducing bugs in existing functionality |
| Shotgun surgery | A single new requirement forces changes across multiple files/classes |
| Ripple effects | One change cascades through dependent modules, requiring retesting of the entire system |
| Merge conflicts | Multiple developers modifying the same file to add different features |
| Regression risk | Every modification to stable code is a potential regression |
Without OCP, adding a new payment method means editing PaymentProcessor, updating every switch statement, modifying tests, and hoping nothing breaks. With OCP, you write a new PaymentMethod implementation and register it - zero changes to existing code.
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| Module has a history of frequent extensions | Simple CRUD with unlikely variation |
| Multiple developers work on the same component | Prototyping / throwaway code |
| You can identify a clear axis of change | Only one or two concrete cases exist and won't grow |
| Plugin/provider architectures | Performance-critical hot paths where indirection costs matter |
| Public APIs / libraries consumed by others | Internal utilities with a single caller |
| Domain has well-understood variation points | Requirements are completely unknown (premature abstraction) |
Rule of thumb: Apply OCP at the second variation. The first time you need a new type, refactor toward OCP. Don't pre-engineer abstractions for hypothetical futures (see YAGNI tension below).
Key Concepts & Theory
Abstraction as the Key Mechanism
OCP is achieved through strategic abstraction. You identify the axis of change - the dimension along which requirements will vary - and introduce an interface at that boundary.
Polymorphism
Runtime polymorphism (virtual dispatch) allows client code to operate on the abstraction while concrete implementations vary freely. The client is closed to modification because it never references concrete types.
Plugin Architecture
OCP at the architectural level produces plugin systems. The host application defines extension points (interfaces), and plugins implement them. Examples: VS Code extensions, webpack loaders, Spring beans.
Dependency Inversion Connection
OCP and the Dependency Inversion Principle (DIP) are deeply linked. DIP says "depend on abstractions, not concretions" - which is exactly the mechanism that makes OCP possible. You cannot have OCP without DIP.
The "Protected Variation" Perspective
Craig Larman calls this Protected Variations: identify points of predicted variation and create a stable interface around them to shield the rest of the system.
ASCII Class Diagram
Violation - Switch/If-Else Chain
┌─────────────────────────┐
│ AreaCalculator │
├─────────────────────────┤
│ + calculate(shape) │
│ if shape == "circle" │
│ return π * r² │
│ if shape == "rect" │
│ return w * h │
│ if shape == "tri" │ ← Must MODIFY this class
│ return 0.5 * b * h │ for every new shape
└─────────────────────────┘
▲
│ depends on concrete types
┌────────┴────────┐
│ Circle, Rect, │
│ Triangle ... │ ← No common contract
└─────────────────┘
Correct - Polymorphic Design
┌──────────────────┐
│ «interface» │
│ Shape │
├──────────────────┤
│ + area(): float │
└────────┬─────────┘
│ implements
┌─────────┼──────────┐
│ │ │
┌──────┴───┐ ┌──┴─────┐ ┌──┴──────┐
│ Circle │ │ Rect │ │Triangle │ ← Add new shapes
├──────────┤ ├────────┤ ├─────────┤ WITHOUT modifying
│ + area() │ │+ area()│ │+ area() │ AreaCalculator
└──────────┘ └────────┘ └─────────┘
┌─────────────────────────┐
│ AreaCalculator │
├─────────────────────────┤
│ + calculate(Shape s) │
│ return s.area() │ ← CLOSED for modification
└─────────────────────────┘
Pseudocode Implementation
BAD - If-Else Chain (Violates OCP)
class AreaCalculator:
function totalArea(shapes[]):
total = 0
for shape in shapes:
if shape.type == "circle":
total += PI * shape.radius ^ 2
else if shape.type == "rectangle":
total += shape.width * shape.height
else if shape.type == "triangle":
total += 0.5 * shape.base * shape.height
// Adding a new shape? MODIFY this method ↑
return total
Problem: Every new shape requires editing totalArea(). This class is never "done."
GOOD - Polymorphic Design (Respects OCP)
interface Shape:
function area(): float
class Circle implements Shape:
radius: float
function area(): return PI * radius ^ 2
class Rectangle implements Shape:
width, height: float
function area(): return width * height
class Triangle implements Shape:
base, height: float
function area(): return 0.5 * base * height
// CLOSED for modification - never changes
class AreaCalculator:
function totalArea(shapes: Shape[]):
total = 0
for shape in shapes:
total += shape.area() // polymorphic dispatch
return total
Adding a Pentagon? Write class Pentagon implements Shape - zero changes to AreaCalculator.
Strategy Pattern Connection
OCP is the principle; Strategy is a pattern that implements it:
interface DiscountStrategy:
function apply(order): float
class SeasonalDiscount implements DiscountStrategy:
function apply(order): return order.total * 0.10
class LoyaltyDiscount implements DiscountStrategy:
function apply(order): return order.total * 0.15
// OrderProcessor is CLOSED - new discounts = new strategy classes
class OrderProcessor:
strategy: DiscountStrategy
function checkout(order):
discount = strategy.apply(order)
return order.total - discount
Real-World Examples
1. Payment Processing
interface PaymentGateway:
function charge(amount, token): Result
class StripeGateway implements PaymentGateway: ...
class PayPalGateway implements PaymentGateway: ...
class CryptoGateway implements PaymentGateway: ... // Added later, zero changes to checkout
class CheckoutService:
gateway: PaymentGateway
function processPayment(order):
return gateway.charge(order.total, order.paymentToken)
Business impact: Adding Apple Pay means writing one new class and a config entry - no PR touching core checkout logic.
2. Notification Channels
interface NotificationChannel:
function send(message, recipient): void
class EmailChannel implements NotificationChannel: ...
class SMSChannel implements NotificationChannel: ...
class SlackChannel implements NotificationChannel: ... // Added in sprint 14
class NotificationService:
channels: NotificationChannel[]
function notify(event):
for channel in channels:
channel.send(event.message, event.recipient)
3. Report Generators
interface ReportExporter:
function export(data): bytes
class PDFExporter implements ReportExporter: ...
class CSVExporter implements ReportExporter: ...
class ExcelExporter implements ReportExporter: ... // New requirement, no modification
class ReportService:
function generateReport(data, exporter: ReportExporter):
return exporter.export(data)
Comparison with Similar Patterns
| Aspect | OCP | Strategy Pattern | Template Method | Factory Pattern |
|---|---|---|---|---|
| Nature | Design principle | Behavioral pattern | Behavioral pattern | Creational pattern |
| Mechanism | Abstraction + polymorphism | Composition + delegation | Inheritance + hooks | Encapsulated instantiation |
| Relationship | The why | One way to achieve OCP | Another way to achieve OCP | Supports OCP for object creation |
| Variation | Any axis of change | Algorithm selection at runtime | Steps within a fixed skeleton | Which concrete type to instantiate |
| Coupling | Defines the goal | Low (composition) | Medium (inheritance) | Isolates new from client |
Key insight for interviews: OCP is the principle. Strategy, Template Method, Decorator, and Factory are patterns that help you achieve it. When asked "how do you implement OCP?", answer with the pattern most appropriate to the context.
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| New features via new code - lower regression risk | Increased number of classes/files |
| Easier parallel development (no merge conflicts) | Indirection can reduce readability for newcomers |
| Promotes testability - mock interfaces easily | Premature abstraction if variation never materializes |
| Aligns with plugin architectures | Slight runtime overhead from virtual dispatch |
| Stable public APIs - consumers don't break | Requires upfront identification of variation axes |
| Supports Continuous Delivery - smaller, safer PRs | Can conflict with YAGNI if applied too eagerly |
Constraints & Edge Cases
Over-Abstraction
Creating interfaces for every class "just in case" leads to speculative generality. If there's only one implementation and no foreseeable second, an interface adds noise without value.
When Modification IS the Right Choice
- Bug fixes - fixing incorrect behavior in existing code is modification, and that's fine.
- Refactoring - restructuring internals without changing behavior is healthy modification.
- Breaking changes in early-stage code - before your API has consumers, modification is cheaper than premature abstraction.
- Cross-cutting concerns - sometimes a change genuinely affects all implementations (e.g., adding logging). Modification may be simpler than a decorator chain.
YAGNI Tension
OCP says "design for extension." YAGNI says "don't build what you don't need yet." Resolution:
- First occurrence - write the concrete implementation directly.
- Second occurrence - refactor to introduce the abstraction (Rule of Three).
- Known variation - if the domain guarantees variation (e.g., multiple payment providers), abstract immediately.
Sealed/Final Classes
Some designs intentionally close classes for extension (e.g., sealed in Kotlin/C#). This is a deliberate trade-off: preventing fragile base class problems at the cost of extensibility. OCP doesn't mean everything must be extensible - only the identified variation points.
Interview Follow-ups
Q1: How does OCP relate to the Dependency Inversion Principle?
Model Answer: OCP and DIP are two sides of the same coin. OCP states the goal - modules should be extendable without modification. DIP provides the mechanism - depend on abstractions, not concretions. Without DIP, high-level modules would reference concrete low-level classes directly, making extension impossible without modification. When you inject an interface rather than a concrete class, you simultaneously satisfy both principles. In practice, any OCP-compliant design will also be DIP-compliant.
Q2: Give an example where applying OCP would be harmful.
Model Answer: Consider a startup's MVP with a single payment provider (Stripe). Creating a PaymentGateway interface, a StripeGateway implementation, a factory, and a configuration layer adds 4 files and indirection for zero current benefit. If the startup pivots before ever adding a second provider, that abstraction was pure waste. The correct approach is to write the Stripe integration directly, and refactor toward OCP only when a second provider becomes a real requirement. This respects both OCP and YAGNI by applying abstraction at the point of actual variation.
Q3: How would you refactor a 500-line switch statement to respect OCP?
Model Answer: Step-by-step: (1) Identify the common behavior across all cases - this becomes your interface. (2) Extract each case into a class implementing that interface. (3) Replace the switch with a registry (map/dictionary) that maps discriminators to implementations. (4) The consumer now calls registry.get(type).execute() - polymorphic dispatch replaces branching. (5) New cases are added by registering new implementations. This is essentially the Replace Conditional with Polymorphism refactoring from Fowler's catalog. I'd do it incrementally - extract one case at a time, keeping the switch as a fallback until all cases are migrated.
Q4: Can OCP be applied to functional programming?
Hints:
- Higher-order functions as the extension mechanism (pass behavior as arguments)
- Function composition replaces inheritance hierarchies
- Open maps/dictionaries of handler functions instead of switch statements
- Clojure's multimethods and protocols as FP-native OCP
Q5: How do you handle OCP when the interface itself needs to change?
Hints:
- Interface Segregation Principle - keep interfaces small so changes are rare
- Versioned interfaces (
PaymentGatewayV2) for backward compatibility - Default methods (Java 8+, C# 8+) to extend interfaces without breaking implementors
- Adapter pattern to bridge old implementations to new interface
- Accept that some changes are genuinely breaking - OCP minimizes but cannot eliminate modification
Counter Questions to Ask Interviewer
Use these to demonstrate depth and turn the interview into a conversation:
-
"What's the primary axis of change in this system?" - Shows you think about where to apply OCP rather than applying it blindly everywhere.
-
"How many concrete implementations exist today, and how often are new ones added?" - Demonstrates YAGNI awareness; OCP is most valuable when variation is frequent.
-
"Is this a public API with external consumers, or an internal module?" - Public APIs benefit enormously from OCP (backward compatibility); internal code can tolerate modification more easily.
-
"Are there performance constraints that make virtual dispatch problematic?" - Shows awareness of OCP's trade-offs in latency-sensitive systems (game loops, HFT).
-
"Would you prefer composition (Strategy) or inheritance (Template Method) for this extension point?" - Demonstrates knowledge of multiple OCP implementation mechanisms.
References & Whitepapers
-
Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall. - Original formulation of OCP through inheritance.
-
Martin, R.C. (1996). "The Open-Closed Principle." C++ Report. - Modern reformulation using polymorphism and abstraction.
-
Martin, R.C. (2003). Agile Software Development: Principles, Patterns, and Practices. Prentice Hall. - Chapter 9: OCP in the context of agile design.
-
Fowler, M. (1999). Refactoring: Improving the Design of Existing Code. - "Replace Conditional with Polymorphism" refactoring.
-
Larman, C. (2004). Applying UML and Patterns. - "Protected Variations" as an alternative framing of OCP.
-
Gamma, E. et al. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. - Strategy, Template Method, and Decorator patterns as OCP implementations.
Related Topics
- Single Responsibility Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
- Strategy Pattern
- Template Method Pattern
- Factory Pattern
- Decorator Pattern
Last updated: 2026-05-31