Open/Closed Principle (OCP)
Status: Complete
Category: SOLID Principles
Default enforcement: Advisory
Author: PushBackLog team
Tags
- Topic: quality, architecture
- Methodology: SOLID
- Skillset: any
- Technology: generic
- Stage: execution, review
Summary
Software entities — classes, modules, functions — should be open for extension but closed for modification. When new behaviour is required, you add new code rather than changing existing code that already works. This protects existing functionality from regression while allowing the system to grow.
Rationale
The cost of modification
Every time you modify working code, you risk breaking something that currently works. Without comprehensive tests (and even with them), modifications to shared or central logic can have knock-on effects that are difficult to predict. The OCP is a design strategy that minimises the surface area for regression: if you can add a new payment method by adding a StripePaymentProvider class rather than by modifying PaymentProcessor, then PaymentProcessor — and everything that depends on it — cannot break.
Extension points as future-proofing
The OCP doesn’t mean never changing code; it means designing systems with deliberate extension points so that common change scenarios don’t require modifications to stable, tested code. A system designed with OCP in mind looks ahead: what is likely to vary? Those variation points become interfaces or plugin slots. What is unlikely to vary? That logic is encapsulated and sealed.
Robert C. Martin framed it this way: software changes in two directions — fixing bugs (modification is appropriate) and adding capabilities (extension is the goal). The OCP is primarily about capability extension.
The connection to abstraction
OCP is practically inseparable from DIP. “Open for extension” almost always means “depends on an abstraction that can have new implementations added”. If PaymentProcessor depends on IPaymentProvider rather than on StripeClient directly, new payment providers can be added — opening the system — without touching PaymentProcessor — closing it to modification.
Guidance
Identify variation axes first
Before designing extension points, identify what is actually likely to change. Over-engineering OCP into stable code creates pointless abstractions. Ask:
- What types of this thing could we need in the future?
- What parts of this logic are policy (unlikely to change) versus mechanism (likely to vary)?
- What has changed in this area in the last 6 months?
Design extension points for things that demonstrably vary. Leave things closed-but-modifiable until a second case exists that justifies the abstraction.
Common implementation patterns
| Pattern | When to use |
|---|---|
| Strategy | Vary an algorithm or behaviour — pass different implementations of an interface |
| Decorator | Add behaviour around an existing implementation without changing it |
| Plugin / Provider | Register new implementations at a higher level; core code never knows about specifics |
| Template Method | Fix the skeleton of an algorithm in a base class; let subclasses fill in the steps |
The rule of three
Applying OCP prematurely produces speculative abstractions that almost never match what you actually need. A pragmatic approach: the first case is implemented concretely. When a second case arrives, the tension between the two reveals what actually needs to vary. On the third case, you have enough evidence to design an appropriate abstraction. This is often called the “rule of three”.
Examples
Before OCP (brittle)
class ReportGenerator {
generate(type: string, data: unknown) {
if (type === 'pdf') {
// PDF-specific logic
} else if (type === 'csv') {
// CSV-specific logic
} else if (type === 'excel') {
// Excel-specific logic — requires modifying this class
}
}
}
Every new report format requires opening ReportGenerator and modifying it. Tests for PDF and CSV rendering are at risk each time.
After OCP (extensible)
interface ReportFormat {
render(data: unknown): Buffer;
}
class PdfFormat implements ReportFormat { ... }
class CsvFormat implements ReportFormat { ... }
class ExcelFormat implements ReportFormat { ... } // New format — no change to generator
class ReportGenerator {
generate(format: ReportFormat, data: unknown): Buffer {
return format.render(data);
}
}
ReportGenerator never changes. Adding a new format is adding a new class.
Anti-patterns
1. Hardcoded dispatch on type strings or enums
if (type === 'x') ... else if (type === 'y') is the canonical OCP violation. Every new case requires a modification to the dispatch site, not an addition alongside it.
2. Premature OCP / speculative abstractions
Writing an interface with one implementation “just in case” creates complexity without benefit. Abstractions that don’t serve a current second use case are a maintenance cost, not an investment. Defer OCP refactoring until a real second case arrives.
3. Treating bug fixes as extensions
OCP does not mean never modify code. Fixing a bug in existing logic is modification — and it’s correct. OCP governs capability extension, not correction.
4. Extension points too granular
Making every method overridable “for flexibility” produces a system that is theoretically open but practically incomprehensible. Extension points should be coarse-grained and deliberate, not omnipresent.
Related practices
Part of the PushBackLog Best Practices Library. Suggest improvements →