SOLID

Open/Closed Principle

Software entities should be open for extension but closed for modification — add new behavior without editing existing, tested code.

Extend, don't edit.

The Open/Closed Principle (OCP) says you should be able to add new functionality by adding new code, not by changing code that already works. Editing existing, tested code risks breaking it; adding new code that plugs into an extension point is safer.

Example: instead of a calculateArea function with a growing if shape == ... chain (edited every time you add a shape), define a Shape interface with an area() method and add a new class per shape.

bad.py
# Problem: closed to extension — every new shape edits this function.
def area(shape):
    if shape.kind == "circle":
        return 3.14 * shape.r ** 2
    elif shape.kind == "square":
        return shape.side ** 2
    # add triangle? edit again. Risk of breaking old cases.
good.py
# Fix: open for extension (new classes), closed for modification.
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self): ...

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return 3.14 * self.r ** 2

class Square(Shape):
    def __init__(self, side): self.side = side
    def area(self): return self.side ** 2

def total_area(shapes):
    # Stable: new Shape subclasses need no edits here.
    return sum(s.area() for s in shapes)

Extension points via abstraction.

OCP is achieved with abstractions and polymorphism: depend on a stable interface, and add new implementations to extend behavior. New requirement → new class, existing code untouched.

Strategic closure and its limits.

  • You can't be closed against everything — pick the axes of change most likely to vary and build extension points there.
  • Predicted vs speculative — over-applying OCP creates needless abstraction (YAGNI); add seams when change is real or likely.
  • Mechanisms — polymorphism, Strategy, plugins, configuration, and the Template Method pattern.
  • Protected variation — the GRASP name for the same idea: wrap points of variation behind a stable interface.

Interview Questions

Filter

Software entities (classes, modules, functions) should be open for extension but closed for modification — you should be able to add new behavior without changing existing, working code.

That the entity's behavior can be extended — you can make it do new things — typically by adding new code (a new subclass/implementation) that plugs into an existing abstraction.

That you extend behavior without altering the existing source code of the entity, so already-tested, working code stays untouched and can't be inadvertently broken.

It can introduce regressions in behavior that already worked and was tested, requires re-testing, may affect many callers, and increases merge conflicts. Adding new code that extends via a stable interface avoids disturbing proven code.

Polymorphism via abstraction: define an interface/abstract class and add new implementations. Callers depend on the abstraction, so new behavior arrives as new classes without editing the callers.

A growing if/else or switch on a type/kind that you must edit every time a new case is added — e.g. adding a payment method means modifying the central payment-processing switch.

Introduce an abstraction (interface/base class) with a method for the varying behavior, create one implementation per case moving the branch logic into it, and have callers depend on the abstraction and invoke it polymorphically. Use a factory/registry to construct the right implementation from input. Adding a new case then means adding a class (and registering it), with no edits to the calling code — replacing "modify the switch" with "extend with a new type."

No. OCP is about closure against specific, anticipated kinds of change, not all change. You choose the axis of variation most likely to change (e.g. new shapes, new payment types) and build an extension point there; you remain "open" to other changes you didn't foresee, which may require modification. Trying to be closed against everything leads to over-abstracted, unusable designs. It's strategic, not absolute.

Strategy encapsulates a varying algorithm behind an interface and injects it into a context. To add a new behavior you add a new strategy class implementing the interface and plug it in — the context and its callers don't change. This makes the context open for extension (new strategies) but closed for modification, and lets behavior even change at runtime.

They're the enablers. Abstraction defines a fixed extension point (interface); polymorphism lets new implementations be used through it without callers knowing the concrete type. The abstraction is the "closed" contract; the pluggable implementations are the "open" extension. Without abstraction/polymorphism you'd extend behavior by editing conditionals (modification), violating OCP.

Template Method defines an algorithm's fixed skeleton in a base class (closed for modification) while delegating specific steps to overridable hook methods that subclasses implement (open for extension). New variations extend the algorithm by subclassing and overriding hooks without touching the skeleton, achieving OCP via inheritance rather than composition.

Not necessarily. Adding entirely new functionality (a new method that doesn't change existing behavior) is generally fine and often unavoidable. OCP is chiefly about not having to modify existing behavior/logic to accommodate new variations of a known extension axis. The concern is editing existing code paths (like a switch) that are already relied upon, not additive growth. Pragmatism applies — OCP targets the volatile parts, not every addition.

A plugin architecture defines interfaces the host depends on and loads implementations at runtime, so new capabilities are added as new plugins without recompiling/modifying the host — the ultimate OCP. Dependency injection supplies implementations of abstractions from outside, so you can extend or swap behavior by wiring in a different implementation rather than editing the consumer. Both push variation to the composition edges, keeping core code closed.

Apply OCP where change is demonstrated or highly likely along a known axis — informed by domain knowledge and history (which parts have churned, where new variants keep appearing). Don't add abstractions speculatively for changes you merely imagine (YAGNI); premature extension points add indirection and cost with no payoff and may guess the wrong axis. A pragmatic approach: write the simple version first, and when the second or third variation arrives (rule of three), refactor to introduce the extension point the real cases reveal. So OCP is reactive to real volatility, balancing the cost of future modification against the cost of present abstraction.

Protected Variation is a GRASP principle: identify points of predicted variation or instability and create a stable interface around them to shield the rest of the system from those changes. It's essentially the general form of OCP (and DIP): wrap what varies behind an abstraction so change is contained behind the interface. Framing it as "protect against variation" emphasizes the design decision — you deliberately choose which variations to encapsulate — which helps avoid the trap of blindly applying OCP everywhere. It unifies OCP, DIP, and information hiding under one intent: isolate change.

There's tension: adding a method to a widely-implemented interface forces every implementer to change (a modification), seemingly violating OCP and breaking clients. Techniques to preserve OCP/compatibility: use default methods so existing implementers aren't forced to change; create a new sub-interface (e.g. FooV2 extends Foo) that new code depends on; or use the Extension/Visitor patterns. This reveals OCP's limit — interfaces are hard to extend without touching implementers — which is why you keep interfaces minimal and stable, and design extension along the implementation axis (add new implementing classes) rather than by growing the contract.

OCP via polymorphism makes adding new types easy (add a class implementing the interface — existing code untouched) but adding a new operation to all types hard (you must modify every class or the interface). This is the expression problem: you can be open along one axis at the expense of the other. If operations vary more than types, a Visitor/pattern-matching approach is "closed" against new operations instead. So OCP doesn't give you free extensibility in both dimensions; you choose which axis to optimize based on expected change, and recognize the trade-off rather than assuming polymorphism makes everything extensible.

Architecturally, OCP guides dependency direction so that high-level policy is protected from changes in low-level detail: stable, abstract core components are depended upon by volatile ones, not vice versa (this is why clean/hexagonal architecture points dependencies inward toward abstractions). New adapters (databases, UIs, external services) can be added as plugins implementing core-defined ports without modifying the business core. Component-level principles (Stable Abstractions/Dependencies) formalize it: the most-depended-on components should be stable and abstract, so they rarely need modification while the system is extended at its edges. OCP thus shapes where change is allowed to happen (the periphery) versus protected (the core).

Every extension point adds an abstraction layer: more interfaces, indirection, and cognitive overhead; harder navigation (the real behavior is scattered across implementations and wired at runtime); and potential performance cost from dynamic dispatch. If you guess the wrong variation axis, the abstraction is useless or even obstructive, and refactoring it out is costly. Aggressive OCP can produce "framework-itis" — over-generalized code that's flexible in ways nobody needs and rigid where change actually occurs. The mitigation is to apply OCP selectively to genuinely volatile areas, keep abstractions thin and earned by real requirements, and accept that some future changes will require modification — that's cheaper than pervasive speculative flexibility.

Work incrementally behind characterization tests. First capture current behavior with tests so refactoring is safe. Identify the dominant variation (the switch/if-chain that changes most). Introduce an abstraction (Replace Conditional with Polymorphism): create an interface for the varying operation, extract each branch into an implementation, and route through a factory/registry. Migrate call sites to depend on the abstraction. Do this one conditional/feature at a time (strangler-style), leaving the old path until fully replaced, then delete it. Prioritize areas with high churn where OCP pays off, and avoid abstracting stable code. The goal is to convert "edit central logic for each new case" into "add a new implementation," verified continuously by the tests.

Bertrand Meyer coined it in 1988. Robert C. Martin later popularized the modern, polymorphism-based interpretation as the "O" in SOLID.

No. Bug fixes and legitimate changes still happen. OCP targets a specific pain: having to edit stable, working code every time a new variation of a known kind is added. It steers you to add such variations as new code instead.

Inheritance is one way to extend behavior without modifying the base (override or add in a subclass). But composition/interfaces are often preferred over inheritance for OCP, since deep inheritance can be fragile; both achieve "extend without editing existing code."

By moving variation out of code and into data/config, you can add or change behavior without editing (and redeploying) logic. Examples: a rules engine driven by a config file, a lookup table mapping types to handlers, or feature flags. The code that reads the config stays closed; new behavior is added by registering data. This is powerful but has limits — complex logic doesn't fit neatly in config, and over-configurability becomes its own maintenance burden.

When you extend via new implementations, something must choose/create the right one from input. A factory or registry centralizes that mapping. With a registry, new implementations can self-register (or be registered at startup) so even the factory doesn't need editing for each new type — you add a class and a registration line. This preserves OCP end-to-end: both the calling logic and ideally the creation logic stay closed while the set of implementations grows.

No — polymorphism is the most common mechanism, but OCP is a goal that can be met many ways: inheritance/Template Method, composition/Strategy, plugins, dependency injection, configuration/data-driven behavior, higher-order functions, and even language features like extension methods. The unifying idea is "provide a stable extension point so new behavior is added, not existing behavior edited." The mechanism should fit the situation.

Instead of an interface with implementations, FP passes behavior as functions. A function like process(items, transform) is open for extension — callers supply any transform — while the processing logic stays closed. This is the functional analog of Strategy: the varying behavior is a first-class function argument. Combined with pattern matching or a map of functions keyed by type, you get extensible behavior without editing the core. The trade-off mirrors OOP's expression problem: passing functions makes adding new behaviors easy, while sum types + pattern matching make adding new operations easy but new cases require editing every match — so FP faces the same axis-of-change choice.

A plugin architecture is OCP at the system scale: the host defines stable extension interfaces and discovers/loads implementations at runtime, so the product is extended by shipping plugins, never by modifying the host. This aligns with component principles like the Stable Abstractions Principle (the more a component is depended upon, the more abstract and stable it should be) and Stable Dependencies (depend in the direction of stability). The host's plugin interfaces are the most-depended-on, so they're kept abstract and change-resistant; volatility lives in the plugins at the periphery. Getting this right means new capabilities cost a new plugin, not a core release — but it requires carefully designing the extension contract up front, and a wrong contract is expensive to change since everything depends on it.

When the abstraction cost outweighs the benefit: the variation axis is unknown or unstable (you'd likely guess wrong), there's only ever one implementation with no realistic second, the code is throwaway/rarely-changing, or an extension point would add indirection that hurts readability more than a future edit would cost. In those cases a simple, direct implementation (even a switch) is cheaper and clearer, and you refactor toward OCP only when a real second/third variation appears (rule of three). Blindly enforcing OCP everywhere is over-engineering; the principle is a response to demonstrated volatility, so it's legitimate — often wise — to skip it until the change pressure is real.

OCP's promise — "extend by adding a new subtype, existing code untouched" — only holds if the new subtype is genuinely substitutable, i.e. obeys LSP. If a new implementation strengthens preconditions, weakens postconditions, or throws unexpected exceptions, the existing (closed) code that consumes the abstraction will break when handed it, forcing you to modify that code (adding type checks/special cases) and thereby breaking OCP. So LSP is the correctness precondition for OCP: extension via polymorphism is only safe when subtypes honor the base contract. Together they say: design a stable abstraction (OCP) and ensure every implementation faithfully fulfills it (LSP), so growth is purely additive.