Open Closed Principle (OCP)

Open to extension, closed to modification. The strategy-pattern motivation, said as a rule.

Concept Foundational
8 min read
solid ocp extension polymorphism

Summary#

The Open Closed Principle is the O in SOLID and the one most likely to be quoted as a slogan without the second half. Bertrand Meyer’s formulation, lightly modernised: a module should be open for extension, but closed for modification.

The trap is reading the two halves as a contradiction. They are not. Closed for modification means the existing source — already written, already tested, already shipped — does not need to be edited when a new variant of behaviour shows up. Open for extension means new behaviour can be added by writing new code, typically a new class that plugs into an existing seam. The seam is the whole game: OCP is the principle that says design the seam before you need it, then everyone who extends the system writes additions, not edits.

In practice this is the strategy-pattern motivation said as a rule. When if/switch over a type tag would otherwise grow with every new variant, OCP names the smell and points at the refactor.

Why it matters#

OCP earns its keep when a system has to absorb new variants over time — new payment methods, new file formats, new pricing rules, new discount campaigns. Without it, every new variant is an edit to a central switch statement, and the blast radius of that edit is the entire file plus everything that imports it.

Three concrete reasons interviewers care:

  • Existing tests stay green. If a change is additive — a new class implementing an existing interface — the tests for the existing classes are unaffected. If the change is an edit to a central method, every test that touches that method is at risk.
  • Plugin boundaries get drawn. OCP is the principle that turns a monolith into an extensible core plus a set of replaceable parts. Browser extensions, CI plugins, payment gateways — all are OCP at the architecture level.
  • The team that owns the variant owns the file. When a third party can add a new payment method by writing a new PaymentMethod implementation in their own module, the core team does not become a bottleneck for every new integration.

The principle’s value is measured at the diff level too: when feature X arrives, is the change a new file or an edit to an existing one? OCP rewards the first; it warns against the second.

How it works#

Spotting the violation#

The most reliable detector is the switch over a type tag shape. Look for an if/else-if ladder or a switch keyed on a string, enum, or instance check, where each branch holds behaviour rather than dispatch. Each new variant of the system requires another branch — that is the closed-for-modification half being broken.

A worked example. A draft invoice printer that supports multiple discount campaigns:

public final class InvoicePrinter {
public double finalPrice(double subtotal, String campaign) {
if (campaign.equals("BLACK_FRIDAY")) {
return subtotal * 0.6;
} else if (campaign.equals("LOYALTY_10")) {
return subtotal * 0.9;
} else if (campaign.equals("STUDENT")) {
return subtotal - 5.0;
} else if (campaign.equals("NONE")) {
return subtotal;
}
throw new IllegalArgumentException("Unknown campaign: " + campaign);
}
}

Every time marketing invents a new campaign, this file is edited. The tests for BLACK_FRIDAY are rerun because the file changed. The reviewer of the new branch has to read the whole class. And the campaign rules — which belong to marketing — are baked into a class that the data team owns. That is the OCP smell.

The refactor#

Introduce a seam: a DiscountPolicy interface that each campaign implements. The printer dispatches through the seam; new campaigns arrive as new files.

public interface DiscountPolicy {
double apply(double subtotal);
}
public final class BlackFridayDiscount implements DiscountPolicy {
@Override public double apply(double subtotal) { return subtotal * 0.6; }
}
public final class LoyaltyDiscount implements DiscountPolicy {
@Override public double apply(double subtotal) { return subtotal * 0.9; }
}
public final class StudentDiscount implements DiscountPolicy {
@Override public double apply(double subtotal) { return subtotal - 5.0; }
}
public final class NoDiscount implements DiscountPolicy {
@Override public double apply(double subtotal) { return subtotal; }
}
public final class InvoicePrinter {
public double finalPrice(double subtotal, DiscountPolicy policy) {
return policy.apply(subtotal);
}
}

InvoicePrinter is now closed. A new campaign — say SpringSaleDiscount — is a new class in a new file. The printer is not edited, its tests are not rerun, and the marketing team can own the campaign files without coordinating diffs with the billing team.

What still has to change#

OCP is closed for modification of existing variants. Adding a new variant still requires a small edit somewhere — usually the factory or registry that maps a campaign code to a DiscountPolicy instance:

public final class DiscountRegistry {
private final Map<String, DiscountPolicy> byCode = new HashMap<>();
public DiscountRegistry() {
byCode.put("BLACK_FRIDAY", new BlackFridayDiscount());
byCode.put("LOYALTY_10", new LoyaltyDiscount());
byCode.put("STUDENT", new StudentDiscount());
byCode.put("NONE", new NoDiscount());
}
public DiscountPolicy resolve(String code) {
DiscountPolicy p = byCode.get(code);
if (p == null) throw new IllegalArgumentException("Unknown campaign: " + code);
return p;
}
}

The registry is the one place that knows the full list. Some frameworks remove even this edit via classpath scanning or service-loader registration — but in a hand-rolled design, accept the one-line addition to the registry and call it a win. The closed bit is the behaviour, not the table.

The other side — when OCP has been over-applied#

The opposite failure mode is the speculative abstraction: introducing a DiscountPolicy interface when there is only ever going to be one discount, or worse, building a plugin framework for variation that never arrives.

Under-applied (switch ladder). Every new variant edits one file. Reviews are heavy. The file is owned by whoever blinks first.

Over-applied (interface-for-one). A DiscountPolicy interface, one implementation, one consumer. A factory that returns the only thing it can return. Future-proofing for a future that never comes.

The signal that OCP has been over-applied: an interface with exactly one implementation, no test double in use, and no plausible second variant in the backlog. Inline it. The interface can come back the first time a second variant lands.

Variants and trade-offs#

A few useful refinements:

  • Inheritance vs composition. Meyer’s original formulation leaned on inheritance — extend a class to add behaviour. The modern reading prefers composition: extend a system by adding a class that implements an interface. The composition variant is the one that pairs with DIP and LSP cleanly.
  • The axis of variation must be real. OCP only buys you something along axes where variants actually arrive. Choosing the wrong axis — abstracting over storage when variation is in pricing — leaves you with a flexible system that is rigid in exactly the wrong place.
  • Open does not mean unbounded. A DiscountPolicy is open to new policies, not to “anything anyone wants to do to an invoice.” The seam constrains as much as it permits, and that is the source of its testability.
ApproachWhen it fitsRisk
switch over enum / type tagTiny, stable set of variants (≤ 3) that change rarelyGrows linearly with every new variant; every change touches the central file
Strategy interface + registryThe usual right answer when variation is realRequires picking the right axis; an extra indirection on every call
Plugin SPI (service loader)Variants live in separate modules / third-party JARsHeavy machinery; only earns its keep at framework scale

When this is asked in interviews#

Three moves carry OCP discussion in an LLD round:

  • Name the axis of variation explicitly. Before sketching the interface, say out loud “the thing that varies here is the discount calculation; everything else stays the same.” Interviewers reward this because it shows you are not abstracting blindly.
  • Resist abstracting the second axis. If the interviewer adds a twist — “what if the email template also varies?” — note it, but do not immediately introduce a second interface. Sometimes the right answer is two parallel hierarchies; sometimes one strategy carrying both pieces. Ask which way the variation arrives in production.
  • Defend the switch when it is right. If there are exactly two payment methods and there will only ever be two — say, cash and card in a vending-machine prompt — a switch is fine. Knowing when OCP is overkill is the senior signal, the mirror image of knowing when SRP is over-applied.

The principle’s one durable test, after the meeting: can a new variant land as a new file, or does it require editing a file that already exists? If the latter, the seam is wrong or missing.

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.