Open/Closed Principle (OCP)
Open for extension, closed for modification — add new behaviour without changing stable code.
Why It Matters
- Fewer regressions — Old behaviour stays untouched when you add the Nth variant.
- Parallel work — Team A adds
CryptoDiscountPolicywhile Team B addsPartnerDiscountPolicy— no merge fight in one god method. - Test stability — Existing tests for
OrderPricerstay green; you only add tests for the new policy. - Library mindset — Framework code stays small; product-specific rules live in extensions.
In Layman's Terms
Think of a power strip with sockets. When you buy a new appliance you plug in an adapter — you do not cut open the strip and solder wires every time.
OCP asks your codebase for the same idea: the socket (abstraction) stays fixed; each plug (implementation) is its own thing.
Real-World Analogy
🎛 Plug-in architecture
Browsers don't recompile when someone ships a new video codec — they load a plugin or delegate to an implementation behind a stable API.
Your discount engine, notification sender, or payment gateway should work the same way: stable orchestration code + swappable policies.
Visual
discountFor(Customer c) {
if (c.isVip()) return 0.20; // +new tier? edit here
else if (c.isNew()) return 0.10; // +wholesale? edit here
else return 0.0; // risk: merge conflicts, regressions
} interface DiscountPolicy { BigDecimal rate(Customer c); }
// New VIP tier? → new VipDiscountPolicy implements DiscountPolicy
// New wholesale? → new WholesalePolicy implements DiscountPolicy
// Core pricer → never changes when you add a policyLeft: every new business rule edits one fragile method. Right: new rules ship as new classes behind one interface.
When to Use
- You expect many variants of the same decision (payment methods, notification channels, pricing rules).
- The core flow is stable but business asks for new types every sprint.
- You already see `switch (type)` or long `if / else if` chains growing in one class.
- You publish an API or SDK — callers extend via plugins, not by patching your source.
When NOT to Use
- Only one implementation exists and will stay that way — YAGNI; a simple class is enough.
- The behaviour is truly one-off — avoid abstract factories for a single
if. - Premature abstraction: extract an interface after the second duplication (Rule of Three).
Anti-Pattern (What NOT to Do)
public final class NotificationSender {
public void send(User u, String channel, String body) {
switch (channel) {
case "email" -> smtpSend(u.getEmail(), body);
case "sms" -> twilioSend(u.getPhone(), body);
case "push" -> fcmSend(u.getDeviceToken(), body);
// Adding Slack? WhatsApp? Edit here — OCP violation.
default -> throw new IllegalArgumentException(channel);
}
}
private void smtpSend(String to, String msg) { /* ... */ }
private void twilioSend(String phone, String msg) { /* ... */ }
private void fcmSend(String token, String msg) { /* ... */ }
}The Right Way
public interface NotificationChannel {
boolean supports(String channelId);
void send(User u, String body);
}
public final class NotificationSender {
private final List<NotificationChannel> channels;
public NotificationSender(List<NotificationChannel> channels) {
this.channels = List.copyOf(channels);
}
public void send(User u, String channelId, String body) {
channels.stream()
.filter(c -> c.supports(channelId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(channelId))
.send(u, body);
}
}
// New Slack channel = new class SlackChannel implements NotificationChannel — zero edits above.NotificationSender stays stable; new behaviour ships as new implementations.Pros & Cons
- Stable core — fewer accidental breaks when extending.
- Maps cleanly to Strategy, Template Method, Decorator, Chain of Responsibility.
- Easier to reason about what changed in code review — one new file vs one messy diff.
- More types and indirection — jumping between classes to read the full story.
- Wrong abstraction traps you — if the interface doesn't fit the next variant, you refactor.
- Team discipline needed — resist stuffing new logic back into the switch "just this once".
Switch statement vs Strategy (OCP lens)
| Aspect | Big switch / if-chain | Strategy + registry |
|---|---|---|
| Add new variant | Edit central method | Add new class |
| Risk to old paths | Regression on every edit | Old code untouched |
| Merge conflicts | Everyone edits same method | Different files |
| Testing | Must retest all branches | Test only new strategy |
| When it's OK | 2–3 fixed cases forever | Many variants or frequent additions |
Common Confusions Cleared Up
OCP says design extension points where change is likely. YAGNI says don't build those points until you need them. Use Rule of Three: after the second copy-paste or second branch in the switch, extract the abstraction.
SRP splits reasons to change across classes. OCP keeps existing classes closed while you extend elsewhere. They work together: SRP gives you small units; OCP keeps those units from turning into edit magnets.
Common Mistakes / Anti-Patterns
- "Temporary"
ifthat lives for five years. - Abstract
Processorwith one implementation — extract when the second appears. - Copy-paste entire methods instead of extracting a shared abstraction.
Interview Q&A
Likely Follow-up Questions
- When would you prefer inheritance vs composition for OCP?
- How does Spring's `@ConditionalOnProperty` relate to extension?
- What breaks OCP in a microservice? (shared library everyone edits for each feature)
- Explain Template Method vs Strategy for the same pricing flow.
Quick Cheat Sheet
switch (kind) / if (type) growing every sprint.