skip to content

Open/Closed Principle

The goal is to add new behavior by adding code rather than editing code that already works, using polymorphism, strategies or plugin hooks. The interesting half is knowing when chasing OCP turns into speculative machinery nobody needed.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

What does the Open/Closed Principle (the "O" in SOLID) mean by "open for extension, closed for modification"?

level: juniorimportance: must knowfreq 82%

answer

  1. Add new code, don't edit old code
  2. Abstraction = the hinge: closed core, open implementations
  3. Kill the switch-on-type; each branch becomes a class
  4. Closed only against the anticipated axis of change
  5. Meyer = subclass; Martin = interface + polymorphism

basics

~10 s

You should be able to add new behavior to a module by adding new code (a new class or plug-in), without editing and re-testing the existing, already-working code.

solid answer

~50 s

The Open/Closed Principle (OCP), the "O" in SOLID, says a software entity — class, module, function — should be **open for extension** (its behavior can be varied for new requirements) but **closed for modification** (its existing source, which is already written, reviewed, tested and shipped, does not have to change to do so). The practical mechanism is an abstraction: the stable code depends on an interface/abstract contract, and new behavior arrives as a new implementation of that contract that gets plugged in. The classic smell that OCP addresses is a growing `if type == A … else if type == B …` or `switch` on a type code, repeated in several places, where every new case forces edits in existing files. The payoff is smaller blast radius: fewer regressions, less re-testing, less merge conflict, independent deployability. The cost is an extra indirection, so it is applied where change is actually expected, not everywhere.

code

typescript · 14 lines
typescript
// Closed for modification: this never changes when a format is added.
interface Renderer { readonly name: string; render(r: Report): Bytes }

class ReportService {
  constructor(private readonly renderers: Map<string, Renderer>) {}
  render(r: Report, format: string): Bytes {
    const renderer = this.renderers.get(format)
    if (!renderer) throw new Error(`no renderer for ${format}`)
    return renderer.render(r)   // polymorphic call = the extension point
  }
}

// Open for extension: a new file, nothing existing edited.
class XlsxRenderer implements Renderer { name = 'xlsx'; render(r: Report) { /* ... */ } }

go deeper

for a junior

State the definition and give the switch-statement-to-interface example. Naming polymorphism as the mechanism is enough.

for a middle

Show the refactor concretely (conditional → strategy/registry), and name decorator and plug-in registration as other routes. Mention that only anticipated axes of change are protected.

for a senior

Contrast Meyer's inheritance formulation with Martin's polymorphic one, tie OCP to DIP, and be explicit about the cost: indirection, speculative generality, YAGNI. Say when you'd deliberately keep an exhaustive switch.

for a principal

Frame OCP as a decision about where variation is expected, applied at module/service/API scale: stable published contracts, plug-in boundaries, backward-compatible schema evolution, and the organizational payoff of teams extending without coordinating edits.

## The words, defined - **Software entity**: any unit of code you can name and reuse — a function, class, module, package, service. - **Open for extension**: the entity's behavior can be made to do something new — handle a new payment type, a new export format, a new discount rule. - **Closed for modification**: to get that new behavior, you do **not** have to open and edit the entity's existing source code. Its file stays byte-identical; you add new files instead. - **Abstraction**: a contract (interface, abstract class, function type, protocol) that names *what* is done without saying *how*. Concrete implementations supply the *how*. - **Polymorphism**: the ability of a single call site (`shape.area()`) to run different code depending on which concrete implementation is behind the reference. This is the engine that makes OCP possible in object-oriented code. At first hearing this sounds contradictory — how can something change without changing? The resolution is that *behavior of the system* changes while *the source of the existing modules* does not. The variation point was designed in ahead of time as an abstraction, and new code slots into it. ## The canonical shape of the problem Imagine a reporting module: ``` function render(report, format): if format == "pdf": ...pdf code... else if format == "csv": ...csv code... else if format == "html": ...html code... ``` Add XLSX and you must edit `render`. Worse, in real systems the same `switch` on `format` appears in three other places (file extension, MIME type, size estimate), so one new format means four coordinated edits — and if you miss one, you get a runtime bug rather than a compile error. This is called **shotgun surgery** (one conceptual change scattered across many files). The OCP-conforming version introduces the abstraction: ``` interface Renderer { name(); mimeType(); render(report) } ``` `render` now looks up a `Renderer` in a registry/map and calls it. Adding XLSX = adding one new file implementing `Renderer` and registering it. Nothing existing is edited, so nothing existing can regress. ## Why "closed" is valuable, concretely 1. **Regression risk**: code you did not touch cannot break (modulo shared state). Tests you already have stay valid. 2. **Review and blast radius**: a reviewer reads one new file, not a diff smeared over ten. 3. **Merge conflicts**: two teams adding two formats touch disjoint files instead of both editing the same `switch`. 4. **Distribution**: if the closed module ships as a compiled library, binary, or separately deployed service, a consumer *cannot* edit it — extension via plug-in is the only option available at all. ## Two historical formulations (both are "OCP") - **Bertrand Meyer (1988)** stated it in terms of **inheritance**: once a class is published and used, you do not modify it; you create a *subclass* that adds or overrides behavior. The original class stays frozen for existing clients. - **Robert C. Martin (1990s, "polymorphic OCP")** restated it in terms of **abstract interfaces**: clients depend on a fixed abstract interface; multiple implementations vary behind it, and the implementation can be swapped without recompiling clients. This is the version practiced today, and it composes with the Dependency Inversion Principle (high-level policy depends on abstractions, not on details). ## How you actually achieve it - **Polymorphism / strategy**: replace conditional-on-type with a call through an interface; each branch becomes an implementation. - **Decorator / wrapper**: wrap an existing implementation to add cross-cutting behavior (caching, retry, logging, metrics) without editing the wrapped class. - **Plug-in / registry hooks**: the core declares an extension point and discovers implementations at startup (service loader, dependency-injection container collecting all beans of a type, a config-driven map). - **Callbacks / higher-order functions**: pass in the varying behavior as a function parameter — the lightweight, language-neutral form of Strategy. - **Data-driven configuration**: move the variable part into data (rule tables, templates) so "new behavior" is a new row, not new code. ## The essential caveat: you cannot be closed against everything No design is closed against *all* possible changes — only against the **axis of change you anticipated**. A renderer registry is closed against "new format" but wide open to modification if the requirement becomes "every render must be async and streamed": that changes the interface itself, and every implementation must change. Choosing which axis to protect is a judgment call informed by what has actually changed before. Guessing wrong produces **speculative generality**: layers of abstraction that serve exactly one implementation forever, costing readability and indirection while buying nothing. The mature reading of OCP is therefore: write the simple direct code first; when the *second* variant appears, refactor to the abstraction; do not build the plug-in framework for a variant nobody has asked for. ## Edge cases and honest limits - **Bug fixes are modifications, and that's fine.** OCP is about extending behavior for new requirements, not a ban on ever editing code. - **Adding a case to a closed set can be worse.** If a type genuinely has a fixed set of variants (e.g. `Success | Failure`), an exhaustive `switch` that the compiler checks may be *safer* than an open interface, because adding a variant then produces compile errors at every place that must be updated rather than silent wrong behavior. - **The expression problem**: interface-based OCP makes it cheap to add new *types* (implementations) and expensive to add new *operations* (a new method forces edits to every implementation). The `switch`-based style is the mirror image. You choose which dimension you expect to grow. - **Runtime openness costs something**: dynamic dispatch defeats some inlining, plug-in discovery complicates startup and static analysis, and "anyone can register anything" widens the security/trust surface.

  • If OCP forbids modifying existing code, how do you ever fix a bug?
    You modify it. OCP targets *new behavior driven by new requirements*, not corrections. A bug means the existing code does not meet its already-agreed contract, so editing it is the right fix — and it does not add a new variation point.
  • Does adding a new method to an existing interface violate OCP?
    Effectively yes, for that interface's implementers: every implementation must now change. That is the expression-problem trade-off — interface-based OCP is closed against new implementations but open to churn when new operations are added. Mitigations are default methods, a second smaller interface (ISP), or an abstract base class.
  • How is OCP related to the Dependency Inversion Principle?
    DIP is the structural rule that makes OCP achievable: high-level policy and low-level details both depend on an abstraction owned by the policy side. Once the dependency points at an abstraction, a new detail can be plugged in without touching the policy — which is exactly OCP.

A wall socket. The house wiring is closed — you never re-wire the wall to use a new lamp. The socket is the fixed contract; any appliance that fits the plug extends what the house can do. But the socket is only open along the axis it anticipated: switch the country's voltage and every appliance breaks, because that change crosses the contract itself.

context

open as a page

You find a billing module where four different functions each contain a `switch` on a `customerType` code (RETAIL, WHOLESALE, GOVERNMENT). Adding a new customer type means editing all four. How do you restructure this to satisfy the Open/Closed Principle, and what exactly becomes "closed"?

level: middleimportance: must knowfreq 68%

basics

~20 s

Create one interface with the four behaviors, and one implementation per customer type holding that type's four branches. The billing code looks up the right implementation and calls it, so a new type is a new class, not four edits.

open as a page

How do you decide when applying the Open/Closed Principle is worth it, versus when adding the abstraction is speculative over-engineering? What signals do you use?

level: seniorimportance: must knowfreq 61%

basics

~20 s

You can't be closed against every change, only the ones you predict. Write simple direct code first; add the abstraction when a second real variant shows up or history says that axis changes. One-implementation interfaces built "just in case" are cost with no payoff.

open as a page

Besides swapping strategy implementations, what other mechanisms let you extend a system's behavior without modifying existing code — and how do the Decorator pattern and plug-in extension points each achieve the Open/Closed Principle?

level: middleimportance: should knowfreq 52%

basics

~20 s

A Decorator wraps an existing object behind the same interface and adds behavior around it (caching, retry, logging) without touching the wrapped class. A plug-in extension point lets the core discover and call implementations it has never heard of, registered at startup or install time.

open as a page

An architect claims a design is "fully closed for modification." Why is that claim impossible in general, and what trade-off does interface-based Open/Closed force you to accept about adding new types versus adding new operations?

level: seniorimportance: should knowfreq 38%

basics

~20 s

You can only be closed against changes you predicted. Using interfaces makes adding a new implementation cheap but adding a new method to the interface expensive — every implementation must change. A big switch statement has the opposite trade-off.

open as a page

How does the Open/Closed Principle apply above the class level — to modules, services, and published APIs — and what mechanisms enforce it at that scale?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

At large scale, being "closed" means other teams and consumers can add capabilities without you changing or redeploying your code: stable published contracts, plug-in boundaries, event streams anyone can subscribe to, and backward-compatible schema evolution.

open as a page