Overview
The Open-Closed Principle (OCP) says software entities should be open for extension but closed for modification: you should be able to add new behavior without editing existing, tested code. In practice, this means depending on abstractions so new cases are new classes.
OCP is the reason Strategy, Decorator, Observer, and plugin architectures exist. It does not mean code is never edited; it means the parts that change often (new payment types, discount rules, file formats) are designed as extension points, so adding one does not ripple through the system.
You add new devices by plugging them in, not by rewiring the strip. The strip is closed for modification but open for extension through its sockets.
When to use it
- A class keeps growing a switch or if/else for new types.
- New variants arrive regularly (discounts, formats, providers).
- Existing behavior is critical and risky to modify.
Where it shows up in interviews
Recognize it when: every new type edits the same method.
- Design a discount engine
- Design a notification service
Recognize it when: third parties add behavior.
- Design a rule engine
- Design a logging framework with appenders
Where it is used in real software
Logback and Log4j add new appenders (file, Kafka, cloud) by implementing an interface, without touching the logger core.
Hosts expose extension points so features are added without changing the host.
Add request behavior by registering new interceptors.
Key terms
- Extension point
- An abstraction where new behavior can plug in.
- Closed for modification
- Existing code does not change for new cases.
- Registry
- Map of keys to implementations for lookup.
How it works, step by step
- 1Identify the axis of change
What new variants keep arriving?
- 2Extract an abstraction for it
DiscountRule.apply(cart).
- 3Move each existing case into a class
PercentageOff, BuyOneGetOne.
- 4Register implementations
List, map, or DI container.
- 5Add new cases as new classes
No edits to the engine.
Adding a new discount
Discount engine supports percentage and flat discounts; add 'buy 2 get 1'
| Design | Files changed | Risk |
|---|---|---|
| switch in DiscountService | DiscountService (modified) | Can break existing discounts |
| DiscountRule interface + list | New BuyTwoGetOne class + registration | Existing rules untouched |
NOWDesign: switch in DiscountService | Files changed: DiscountService (modified) | Risk: Can break existing discounts
OCP localizes change: the new feature lives in a new file.
Implementation
type Cart = { items: { sku: string; qty: number; priceCents: number }[]; coupon?: string }; interface DiscountRule { appliesTo(cart: Cart): boolean; discountCents(cart: Cart): number;} const subtotal = (c: Cart) => c.items.reduce((s, i) => s + i.qty * i.priceCents, 0); class PercentageOff implements DiscountRule { constructor(private code: string, private pct: number) {} appliesTo(c: Cart) { return c.coupon === this.code; } discountCents(c: Cart) { return Math.round(subtotal(c) * this.pct); }} class BuyTwoGetOne implements DiscountRule { // new rule: no existing code edited constructor(private sku: string) {} appliesTo(c: Cart) { return c.items.some((i) => i.sku === this.sku && i.qty >= 3); } discountCents(c: Cart) { const item = c.items.find((i) => i.sku === this.sku)!; return Math.floor(item.qty / 3) * item.priceCents; }} class DiscountEngine { constructor(private rules: DiscountRule[]) {} total(cart: Cart) { const discount = this.rules.filter((r) => r.appliesTo(cart)).reduce((s, r) => s + r.discountCents(cart), 0); return Math.max(0, subtotal(cart) - discount); }}Complexity and performance
Plus registration.
Per request.
Trade-offs
Designing for every possible change adds complexity; apply OCP where variation actually occurs.
Independent rules may need ordering or exclusivity; add priority or composition rules explicitly.
Variants and related techniques
Rules defined in data or a DSL instead of classes.
ServiceLoader or dependency injection discovers implementations.
Common mistakes
- Moving the switch into a factory and calling it OCP.
Fix: A small registry is fine; keep business logic free of type checks.
- Applying OCP before a second variant exists.
Fix: Refactor to an extension point when change actually repeats.
Interview questions
How do you apply OCP to a growing switch statement?
Extract an interface for the varying behavior, move each case into its own implementation, and have the caller iterate or look up implementations; new cases become new classes.
Is it possible to be fully closed for modification?
No. You choose which axes of change to close against, based on what actually varies. Other kinds of change still require edits.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Refactor a shipping-cost switch | Easy | Extract interface. |
| Design an extensible discount engine | Medium | Rule composition. |