SOLID Principles · The Five Principles

Open/Closed Principle

Add new behavior by adding code, not by editing working code - using extension points such as interfaces, strategies and registries.

  • 4 min read
  • 11 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The principle
  3. How to apply it, step by step
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

The Open/Closed Principle says code should be open for extension but closed for modification: when a new case comes along, you add code instead of editing code that already works. It's how plugin systems, payment providers and discount engines grow without every change re-opening (and re-testing) the core. This skill covers extension points, how to refactor a growing switch into them, and when not to bother.

Here's the kind of code it's about:

Java

double discount(Order order, String campaign) {
    switch (campaign) {
        case "SUMMER": return order.total() * 0.10;
        case "BLACK_FRIDAY": return order.total() * 0.30;
        case "LOYALTY": return Math.min(20, order.total() * 0.05);
        default: return 0;
    }
}

Python

def discount(order: Order, campaign: str) -> float:
    match campaign:
        case "SUMMER":
            return order.total() * 0.10
        case "BLACK_FRIDAY":
            return order.total() * 0.30
        case "LOYALTY":
            return min(20, order.total() * 0.05)
        case _:
            return 0

Marketing launches a new campaign every month. Each one edits this method, re-tests every existing campaign, and risks breaking the ones that already work.

We'll go from simple to more technical:

  1. The principle, and the extension point that makes it work.
  2. Refactoring a switch into implementations and a registry, step by step.
  3. Where you'll see it: patterns, plugins and middleware.
  4. Choosing which changes to close against - and which not to.

The principle

Software entities should be open for extension, but closed for modification.

Give the varying behavior an extension point - usually an interface - so new cases are new classes:

Java

interface DiscountRule {
    boolean appliesTo(String campaign);
    double discount(Order order);
}

class Checkout {
    private final List<DiscountRule> rules;
    Checkout(List<DiscountRule> rules) { this.rules = rules; }

    double discount(Order order, String campaign) {
        for (DiscountRule r : rules) if (r.appliesTo(campaign)) return r.discount(order);
        return 0;
    }
}

Python

from abc import ABC, abstractmethod


class DiscountRule(ABC):
    @abstractmethod
    def applies_to(self, campaign: str) -> bool: ...

    @abstractmethod
    def discount(self, order: Order) -> float: ...


class Checkout:
    def __init__(self, rules: list[DiscountRule]):
        self._rules = rules

    def discount(self, order: Order, campaign: str) -> float:
        for r in self._rules:
            if r.applies_to(campaign):
                return r.discount(order)
        return 0

A new campaign is a new DiscountRule class added to the list. Checkout never changes. (In Python, when each rule is a single method, the extension point is often just a callable: a dict from campaign name to function works as well as a class per campaign.)

How to apply it, step by step

  1. Find the part that keeps changing - typically a switch or if chain over "kinds".
  2. Define an interface for what each kind does.
  3. Move each branch into its own implementation.
  4. Let the stable code receive the implementations (constructor, list, registry map).
  5. Adding a kind now means adding a class and registering it - no edits to tested code.

Where you'll see it

  • Strategy, Decorator, Observer and Template Method are all OCP in practice.
  • Plugin systems: IDE extensions, java.util.ServiceLoader (Python: package entry points, loaded with importlib.metadata.entry_points(), and pytest's plugin hooks), Spring picking up every bean that implements an interface.
  • Web frameworks let you add filters/middleware without editing the framework.

Which tool for which job?

The variationExtension point
New kinds of behavior keep arriving (campaigns, formats, providers)an interface plus a registry of implementations
Each case is a single functiona map from key to function (lambda / callable)
Cases differ only in numbers or textconfiguration data, not classes
Behavior added around existing behavior (logging, caching)decorators or middleware
Third parties add behavior without touching your codea plugin mechanism (ServiceLoader, Python entry points)
The set of kinds is fixed, but operations keep cominga sealed type with exhaustive switches (closed on purpose)

Common mistakes

MistakeWhat happensDo this instead
An interface for something with one implementation and no second in sightindirection without benefitadd it when the second variation arrives
A factory with a growing switchthe edit just moveda registry: adding means registering
Closing against the wrong axisthe next change doesn't fit the interfaceclose against changes you've actually seen
Overlapping rules in a first-match listthe result depends on registration orderdisjoint conditions, or explicit priorities
A new class per case that differs only in a numberdozens of near-identical classesput the numbers in configuration

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Your ReportExporter has if (format == PDF) ... else if (format == CSV) .... How would OCP restructure it?

Show answer

Introduce an Exporter interface with one implementation per format, and a registry mapping format -> exporter. A new format is a new class plus a registration; the exporting code doesn't change.

2. Is an interface with exactly one implementation always good OCP design?

Show answer

No. If no second implementation is realistically coming, it's indirection without benefit - add the extension point when variation actually appears.

3. Which other principle does OCP depend on so the stable code can stay unchanged?

Show answer

Dependency Inversion - the stable code must depend on the abstraction, not the concrete implementations.

Key takeaways

  • Add behavior by adding code, not editing tested code.
  • Put an interface where variation keeps happening; register new implementations.
  • Close against the changes you actually expect - not every imaginable one.
  • Strategy, Decorator, Observer and plugins are OCP in action.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

The variationExtension point
New kinds of behaviorinterface + registry
Each case is one functionmap of key -> function
Cases differ only in numbersconfiguration
Behavior around behaviordecorators, middleware
Third-party additionsServiceLoader, entry points
Fixed kinds, growing operationssealed type + exhaustive switch

Snippets worth knowing by heart

Java

interface DiscountRule {
    boolean appliesTo(String campaign);
    double discount(Order order);
}

final class Checkout {
    private final List<DiscountRule> rules;
    Checkout(List<DiscountRule> rules) { this.rules = List.copyOf(rules); }
    double discount(Order order, String campaign) {
        for (DiscountRule r : rules) if (r.appliesTo(campaign)) return r.discount(order);
        return 0;
    }
}

Python

RULES: dict[str, Callable[[Order], float]] = {
    "SUMMER": lambda o: o.total() * 0.10,
    "LOYALTY": lambda o: min(20, o.total() * 0.05),
}

def discount(order: Order, campaign: str) -> float:
    rule = RULES.get(campaign)
    return rule(order) if rule else 0

The traps

  • One-implementation interfaces with no second in sight.
  • A factory switch - the edit moved, it didn't go away.
  • The wrong axis - the next change doesn't fit the interface.

Staff-level points, one line each

  • Meyer (1988): open via inheritance; Martin (1996): polymorphism behind an interface - today's meaning.
  • Strategic closure: close against changes history shows; "fool me once".
  • Discovery can silently miss a plugin: DI scanning, META-INF/services/provides, an unimported Python module - test the registry.
  • Published interfaces grow additively: Java default methods; a new Python @abstractmethod breaks every plugin at instantiation.
  • First-match lists depend on order: SUMMER_FLASH got 10 or 25 depending on registration order.
  • Data-driven rules when cases differ only in parameters - and then the data needs review and audit.

Say it in an interview

  • "That switch changed in every release, so I'd make it an interface with a registry."
  • "This one hasn't changed in years - I'd leave it simple."
  • "New providers are discovered through the registry, and a test checks every provider is registered."
  • "Rules are disjoint by design, and a test fails if two match the same campaign."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Open/Closed Principle free

A Password Policy Built from Plugins

MediumProCoding problemJava · Python

Security wants password policies to be configurable - and new kinds of rule keep being requested. Instead of one growing method full of ifs, each rule is a small PasswordRule class, and a RuleRegistry builds rules from text specs like length:8 or max-repeat:2.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Open/Closed Principle free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime