Back to the map

Module 02 · SOLID and friends · Lesson 2 of 7

Open-closed

New behaviour should arrive as new classes, not edits to working ones. Extension points through interfaces and strategies, the type switch they replace, and why to wait for real variation before abstracting.

CoreMust-knowMid 11 min Updated Nov 2026

Why it matters

Every time a working, tested class is edited to add a feature, there is a chance of breaking what already worked. When the same switch statement over "type" appears in five places, adding a type means finding and editing all five, and missing one is a bug. The open-closed principle aims for designs where the common kind of change, a new variant of something, is made by adding code rather than modifying it.

The idea

A module should be open for extension (its behaviour can be extended) and closed for modification (extending it does not require changing its source). In practice this means identifying the axis along which change is expected and putting an abstraction there: an interface whose implementations can be added without touching the code that uses them.

Checkoutapplies promotions«Promotion»discount(cart)PercentOffBuyTwoGetOneFreeShippingnew: added only
The checkout depends on an interface. Adding a promotion type is a new class registered at startup; the checkout and existing promotions are untouched.

From switch to extension point

// Closed to nothing: every new promotion edits this method and its tests
long discount(Cart cart, PromotionType type) {
    switch (type) {
        case PERCENT_OFF:   return cart.subtotal() * 10 / 100;
        case BUY_TWO_GET_ONE: return cheapestOfEveryThree(cart);
        default: throw new IllegalArgumentException(type.name());
    }
}

// Open for extension: a new promotion is a new class
interface Promotion { long discount(Cart cart); }
record PercentOff(int percent) implements Promotion {
    public long discount(Cart c) { return c.subtotal() * percent / 100; }
}
final class Checkout {
    private final List<Promotion> promotions;          // injected; extended by configuration
    long total(Cart cart) {
        long best = promotions.stream().mapToLong(p -> p.discount(cart)).max().orElse(0);
        return cart.subtotal() - best;
    }
}

The mechanisms are the ones from earlier lessons: polymorphism, interfaces, composition, strategies. Extension can also come from configuration (rules loaded as data), events (new listeners subscribe without the publisher changing) and plugin registration.

Choosing the axis

No design is closed against every change; trying to make it so produces abstraction everywhere and clarity nowhere. Close the design against the changes that are likely: the ones the requirements hint at, or the ones that have already happened. A useful habit is the rule of three: the first time, write the concrete code; the second time a similar variant appears, notice; the third time, or as soon as the variation is clearly a product direction, introduce the extension point.

Sign you need an extension pointSign you do not (yet)
The same switch on a type appears in several placesOne branch with no sign of a second
Product calls the thing a 'type' with more plannedA one-off rule unlikely to vary
Third parties or other teams must add variantsOnly your team will ever touch it
Each new variant has been a risky edit to shared codeEdits have been easy and safe

When modification is fine

Fixing a bug, clarifying code and changing behaviour that genuinely changed are modifications, and that is fine. The principle targets the additive case: new kinds of a thing should not require surgery on the code that handles existing kinds.

Trade-offs

Extension points make the expected change cheap and safe, at the cost of indirection: logic is spread across implementations and a reader must find which ones are registered. Put in the wrong place, they add complexity that never pays off and can even make the actual change harder. Exhaustive pattern matching over sealed types is the counterpart when operations vary more than types, as the Core CS lesson on OOP versus functional styles explains.

In an interview

Tie the principle to a requirement the interviewer gave you: "You mentioned pricing rules change often, so pricing is a PricingStrategy interface; weekend surcharges or electric-vehicle discounts are new strategies and the parking lot code does not change." Then show restraint: "Vehicle types are a small fixed set, so a sealed enum is enough there for now."

Check yourself

Shipping cost is computed in a method with an if-else chain over carrier names, copied into the checkout, the order summary and the returns flow. Adding a carrier keeps breaking one of the three. What change applies the open-closed principle?

Open-closed · Backplane