Open-Closed Principle (OCP)
The Problem It Solves
Discount calculation for a store: festival season adds FESTIVAL10, then BULK5, then loyalty tiers, then coupons. The implementation is a switch statement inside the tested, deployed checkout path. Every new discount edits working code — re-review, re-test, re-deploy the whole flow, and one bad merge in the switch breaks checkout on Black Friday. OCP (Bertrand Meyer): entities should be open for extension, closed for modification — new behavior arrives as new code, not as edits to proven code.
Before / After
BEFORE: variation inside core AFTER: core closed, variants added
┌────────────────────────────┐ ┌───────────────┐
│ Checkout │ │ Checkout │ frozen:
│ if (FESTIVAL10) ... │ │ + Discount │ depends on interface
│ else if (BULK5) ... │ ──────► │ interface │ only
│ else if (LOYALTY) ... │ └──────┬────────┘
│ else ... ← edit per sale │ ▼
└────────────────────────────┘ Festival | Bulk | Loyalty | Coupon
every addition = risky edit each variant = NEW file, zero edits upstream
Mechanics
interface Discount { Money apply(Money subtotal, Cart cart); }
class Checkout {
private final Discount discount; // injected policy
Checkout(Discount d) { this.discount = d; }
Money total(Cart cart) {
Money sub = cart.subtotal();
return sub.minus(discount.apply(sub, cart)); // never edited again
}
}
class FestivalDiscount implements Discount { ... } // sale season: add file,
class CouponDiscount implements Discount { ... } // wire in config. Done.
Checkout has been closed since the extraction; growth is purely additive.
What Counts as Extension
| Mechanism | Closed-core effect |
|---|---|
| Strategy/interface implementations | New behavior = new class |
| Template-method hooks | Subclass overrides hook points |
| Decorator wrapping | Add responsibilities without touching wrapped code |
| Parameterization/config values | Behavior varies by data, not edits |
OCP does not mandate interfaces everywhere — a lookup table or config value can close an axis too.
The Cost Side
- Guessing the wrong axis produces abstraction sprawl: five strategies where two branches would do.
- Every seam costs reading effort and indirection.
- Rule: apply OCP to axes with demonstrated or documented change frequency (“discount rules change monthly” justifies the interface).
Interview Framing
- Precise definition matters: “closed” means no edits to existing tested units when adding behavior — not that files never change ever again (refactoring still happens).
- Strong candidates connect OCP to encapsulate-what-varies: identifying the axis is the prerequisite; the strategy interface is the enforcement.
Failure Modes
- Switch-statement sprawl across many classes (adding a discount means editing N switches) — solved by polymorphism or registry maps replacing conditionals.
- Abstraction built before the second real variant exists — YAGNI applies until change is evidenced.
The Design Question Behind OCP
“Closed for modification” requires knowing where to leave the opening. Extension points are deliberate, named places where future behavior plugs in. Design them wrong and you get either rigid code (no seam where change came) or Swiss-army abstractions (seams everywhere, none used). This page is about choosing seam locations and mechanisms deliberately.
Predicting Variation
Requirements text Likely axis Mechanism
───────────────────────── ───────────────── ────────────────
"card, UPI, wallets..." payment algorithms Strategy
"later: Slack, SMS alerts" notification channels Observer/Strategy
"admin can add new roles" authorization rules Policy objects / RBAC data
"report as PDF and Excel" output formats Template method
"pricing differs per region" configuration values Data-driven lookup (no classes)
The last row matters most in practice: many axes are data, not behavior — a table of region→tax-rate beats a TaxStrategy class hierarchy. OCP is satisfied by parameterization; classes are only needed when logic varies.
Mechanism Selection
| Mechanism | Best when | Cost |
|---|---|---|
| Strategy interface | Algorithm swaps, runtime selection | Client must know variants |
| Template-method hook | Fixed skeleton, varying steps | Inherits inheritance coupling |
| Decorator | Stacking cross-cutting add-ons | Wrapper sprawl if unbounded |
| Plugin registry (map of impls) | Third parties adding variants | Discovery/lifecycle machinery |
| Config/data-driven values | Varying numbers, thresholds, flags | None — cheapest seam |
| Event/callback hooks | Unknown future listeners need notification | Ordering/debugging complexity |
Real-World Seams
- Servlet filters / Spring interceptors: framework core frozen since 2000s; every cross-cutting concern ever added arrived as an inserted filter — OCP at platform scale.
- JDBC: applications never edit driver code; vendors ship implementations of stable interfaces.
- Java streams collectors:
Collectors.toList()etc. — the reduction skeleton closed, collectors extensible.
Designing the Seam Well
- Name it after the axis (
DiscountPolicy, notProcessor2). - Keep the contract minimal (ISP) — fat extension interfaces freeze wrongly.
- Document lifecycle expectations (thread-safety, call frequency) on the interface — plugin authors cannot read your mind.
- Version the contract: adding a default method is backward-compatible evolution; changing signatures breaks every third-party plugin.
Interview Framing
- “Where would you expect this system to grow?” answered from requirement wording (future-tense phrases, alternative lists) demonstrates design foresight.
- The senior qualifier: proposing the cheapest sufficient seam — config value before class hierarchy — shows OCP judgment rather than pattern reflexes.
Failure Modes
- Extension points nobody extends (speculative generality) — remove after evidence they’re dead.
- Seams leaking internal state to plugins (passing mutable internals) — plugins couple to representation; hand them copies or narrow views.
Premium Content
Unlock Open-Closed Principle and all premium lessons with a subscription.
From ₹199.99/year — See plans