Open/Closed Principle
Software entities should be open for extension but closed for modification — add new behaviour by writing new code, not by changing existing code.
★★★★★4/5Inside a codebase — classes, modules, files
How it works
The Open/Closed Principle (OCP) was formulated by Bertrand Meyer in 1988 and popularised by Robert C. Martin as part of SOLID. A module is 'open' when you can add new behaviour to it; it is 'closed' when existing callers are not broken by that addition.
The implementation technique is abstraction: instead of a class knowing the concrete details of all its variants, it depends on an interface. New variants are new classes that implement the interface — no existing class is touched.
A classic example: a DiscountCalculator with a switch statement over discount types. Every new type requires editing the switch. With OCP, DiscountStrategy is an interface; each discount is a class. Adding a new discount is adding a new file — the calculator is untouched and its tests still pass.
OCP is particularly powerful in plugin architectures, where third parties can extend behaviour without having access to (or being able to modify) the core.
Implementation
TypeScript · Go · Rust// ❌ Every new discount type requires editing this method
class DiscountCalculator {
calculate(order: Order, discountType: string): number {
if (discountType === "student") return order.total * 0.10;
if (discountType === "senior") return order.total * 0.15;
if (discountType === "vip") return order.total * 0.20;
// ❌ adding "employee" means opening this file again
return 0;
}
}
// ✓ Add a new discount by adding a new class — nothing existing changes
interface DiscountStrategy {
calculate(order: Order): number;
}
class StudentDiscount implements DiscountStrategy {
calculate(o: Order) { return o.total * 0.10; }
}
class SeniorDiscount implements DiscountStrategy {
calculate(o: Order) { return o.total * 0.15; }
}
class VIPDiscount implements DiscountStrategy {
calculate(o: Order) { return o.total * 0.20; }
}
// ✓ New discount = new file, zero changes to existing code
class EmployeeDiscount implements DiscountStrategy {
calculate(o: Order) { return o.total * 0.30; }
}
class DiscountCalculator {
calculate(order: Order, strategy: DiscountStrategy): number {
return strategy.calculate(order);
}
}Why it matters
Every time you modify an existing class to add a new variant, you risk breaking existing behaviour. OCP lets you extend the system by addition — safer, and it keeps existing tests green.
✓ When to use
- →When a class has a growing switch/if-else over types or variants
- →Plugin architectures where third parties extend behaviour
- →When you want to add new variants without running existing test suites again
- →Frameworks and libraries — callers extend by subclassing or implementing interfaces
✗ When NOT to use
- →Premature abstraction before you have two real variants — YAGNI applies
- →Not all variation is OCP-worthy — sometimes an if statement is the right answer
Trade-offs
New variants are addable without touching existing, tested code
Requires upfront design — the right abstraction must be chosen before extensions arrive
Reduces regression risk — closed modules don't need retesting for new extensions
Too many extension points create 'shotgun surgery' when the interface itself needs changing
Natural fit for plugin and extension architectures
Over-engineering risk — not every class needs to be extensible
In production
Extension API — VS Code's core is closed; extensions add language support, themes, debuggers without modifying the editor
Custom matchers and reporters extend Jest without forking it — open for extension, closed for modification
BeanPostProcessor, ApplicationListener — the container is extensible without requiring source changes
Industry adoption
Related principles
Strategy Pattern
Define a family of interchangeable algorithms behind a common interface so the calling context can swap them at runtime without changing its own code.
Polymorphism
Allow different types to be used interchangeably through a shared interface — so the calling code doesn't need to know which concrete type it's working with.
Interface / Contract
Define what a component must do without dictating how it does it — so implementations can vary freely while callers remain stable.
Liskov Substitution Principle
A subtype must be fully substitutable for its parent — code that works with the base type must work correctly with any derived type, without surprises.