Open/Closed in Java
Open for extension, closed for modification.
The ever-growing switch
Here's a common start: a switch on a type string. Every new payment type means reopening this method, editing working code, and retesting all the old cases too. The switch keeps growing, and so does the risk.
double fee(String type, double amt) {
return switch (type) {
case "card" -> amt * 0.03;
case "bank" -> 1.5;
default -> throw new
IllegalArgumentException(type);
};
}Open for extension, closed for modification
The Open/Closed Principle: add new behavior by adding new code, not by editing code that already works. Put the varying part behind an interface. Each payment type becomes its own implementation, and callers of fee() never change.
interface FeePolicy {
double fee(double amt);
}
record Card() implements FeePolicy {
public double fee(double amt) {
return amt * 0.03;
}
}Plug-in fees
Each fee rule is a separate implementation. What does this print?
interface Fee { int of(int amt); }
void main() {
List<Fee> fees = List.of(
a -> a / 10,
a -> 5);
int total = 0;
for (var f : fees) total += f.of(200);
System.out.println(total);
}25205205
Show the answer
The loop asks each rule for its fee on 200: 200 / 10 = 20, then a flat 5, so 25. A new fee is just another list entry. The loop is closed for modification but the list is open for extension.
Adding "crypto"
case "card" -> amt * 0.03;
case "bank" -> 1.5;
case "crypto" -> amt * 0.05; // edit!Reopens tested code every month. Rewriting it as if/else or moving it to a utility class changes nothing.
record Crypto() implements FeePolicy {
public double fee(double amt) {
return amt * 0.05;
}
}A brand-new class. Nothing that already works is touched.
Sealed types: closing on purpose
A sealed interface does the opposite on purpose: it closes the hierarchy. Adding a new shape is a modification, but in return the compiler checks every exhaustive switch and flags each one you must update. Great when the cases are truly fixed, like AST nodes.
sealed interface Shape
permits Circle, Square {}
String kind(Shape s) {
return switch (s) {
case Circle c -> "round";
case Square q -> "boxy";
};
}What "closed" does NOT mean
"Closed for modification" doesn't mean classes must be final, fields must be private and immutable, or files are locked in version control. It means existing, tested code doesn't need to be edited to support new behavior. It's about where change happens.
Where it pays off
Payment providers, export formats, discount rules, notification channels: anything that grows every sprint. In interviews and reviews, a switch on a type string that keeps growing is the classic OCP smell. But don't abstract everything up front; apply it where change actually keeps coming.
Key takeaways
- New behavior = a new class or lambda, not edits to old code
- Interfaces plus polymorphism are the usual tool
- A switch on a 'type' string that keeps growing is the classic smell
- Sealed types deliberately close a hierarchy in exchange for exhaustive switches
💡 A power strip is open for extension: plug in a new device without rewiring the house.
Bertrand Meyer coined the Open/Closed Principle in 1988 in his book Object-Oriented Software Construction. His version leaned on inheritance; the interface-based version became popular through Robert C. Martin.
Practice questions
What does this print?
interface Discount { int apply(int p); }
void main() {
List<Discount> ds = List.of(
p -> p - 10,
p -> p / 2);
int price = 100;
for (var d : ds) price = d.apply(price);
System.out.println(price);
}- 45
- 50
- 40
- 90
Check your answer
45. The discounts run in list order: 100 - 10 = 90, then 90 / 2 = 45. Adding a new discount means adding a list entry, not changing the loop.
New payment types arrive every month. Which refactor best follows Open/Closed?
double fee(String type, double amt) {
return switch (type) {
case "card" -> amt * 0.03;
case "bank" -> 1.5;
default -> throw new
IllegalArgumentException(type);
};
}- Keep adding cases and write a unit test for each
- Define interface FeePolicy { double fee(double amt); } with one implementation per type
- Replace the switch with nested if/else
- Move the switch into a static utility class
Check your answer
Define interface FeePolicy { double fee(double amt); } with one implementation per type. With an interface, each new payment type is a new class and fee() callers never change. The other options still require editing the same decision logic every month.