SOLID & DESIGN PRINCIPLES / OBJECT DESIGN BRIEF

Open-Closed Principle

The Open-Closed Principle (OCP) says software entities should be open for extension but closed for modification: you should be able to add new behavior without editing existing, tested code.

BeginnerPhase 02 / Topic 2 of 8ResponsibilitiesCollaborationsExtensibility
01

Overview

The Open-Closed Principle (OCP) says software entities should be open for extension but closed for modification: you should be able to add new behavior without editing existing, tested code. In practice, this means depending on abstractions so new cases are new classes.

OCP is the reason Strategy, Decorator, Observer, and plugin architectures exist. It does not mean code is never edited; it means the parts that change often (new payment types, discount rules, file formats) are designed as extension points, so adding one does not ripple through the system.

A power strip

You add new devices by plugging them in, not by rewiring the strip. The strip is closed for modification but open for extension through its sockets.

02

When to use it

  • A class keeps growing a switch or if/else for new types.
  • New variants arrive regularly (discounts, formats, providers).
  • Existing behavior is critical and risky to modify.
03

Where it shows up in interviews

Growing conditionals

Recognize it when: every new type edits the same method.

  • Design a discount engine
  • Design a notification service
Plugin architectures

Recognize it when: third parties add behavior.

  • Design a rule engine
  • Design a logging framework with appenders
04

Where it is used in real software

Logging frameworks

Logback and Log4j add new appenders (file, Kafka, cloud) by implementing an interface, without touching the logger core.

Browser extensions and IDE plugins

Hosts expose extension points so features are added without changing the host.

Spring's HandlerInterceptor

Add request behavior by registering new interceptors.

05

Key terms

Extension point
An abstraction where new behavior can plug in.
Closed for modification
Existing code does not change for new cases.
Registry
Map of keys to implementations for lookup.
06

How it works, step by step

  1. 1
    Identify the axis of change

    What new variants keep arriving?

  2. 2
    Extract an abstraction for it

    DiscountRule.apply(cart).

  3. 3
    Move each existing case into a class

    PercentageOff, BuyOneGetOne.

  4. 4
    Register implementations

    List, map, or DI container.

  5. 5
    Add new cases as new classes

    No edits to the engine.

07

Adding a new discount

Discount engine supports percentage and flat discounts; add 'buy 2 get 1'

Step 1 / 2
DesignFiles changedRisk
switch in DiscountServiceDiscountService (modified)Can break existing discounts
DiscountRule interface + listNew BuyTwoGetOne class + registrationExisting rules untouched

NOWDesign: switch in DiscountService | Files changed: DiscountService (modified) | Risk: Can break existing discounts

OCP localizes change: the new feature lives in a new file.

08

Implementation

type Cart = { items: { sku: string; qty: number; priceCents: number }[]; coupon?: string }; interface DiscountRule {  appliesTo(cart: Cart): boolean;  discountCents(cart: Cart): number;} const subtotal = (c: Cart) => c.items.reduce((s, i) => s + i.qty * i.priceCents, 0); class PercentageOff implements DiscountRule {  constructor(private code: string, private pct: number) {}  appliesTo(c: Cart) { return c.coupon === this.code; }  discountCents(c: Cart) { return Math.round(subtotal(c) * this.pct); }} class BuyTwoGetOne implements DiscountRule { // new rule: no existing code edited  constructor(private sku: string) {}  appliesTo(c: Cart) { return c.items.some((i) => i.sku === this.sku && i.qty >= 3); }  discountCents(c: Cart) {    const item = c.items.find((i) => i.sku === this.sku)!;    return Math.floor(item.qty / 3) * item.priceCents;  }} class DiscountEngine {  constructor(private rules: DiscountRule[]) {}  total(cart: Cart) {    const discount = this.rules.filter((r) => r.appliesTo(cart)).reduce((s, r) => s + r.discountCents(cart), 0);    return Math.max(0, subtotal(cart) - discount);  }}
09

Complexity and performance

Adding a variant1 new class

Plus registration.

EvaluationO(rules)

Per request.

10

Trade-offs

Speculative extension points

Designing for every possible change adds complexity; apply OCP where variation actually occurs.

Rule interaction

Independent rules may need ordering or exclusivity; add priority or composition rules explicitly.

11

Variants and related techniques

Configuration-driven extension

Rules defined in data or a DSL instead of classes.

Plugin loading

ServiceLoader or dependency injection discovers implementations.

12

Common mistakes

  • Moving the switch into a factory and calling it OCP.

    Fix: A small registry is fine; keep business logic free of type checks.

  • Applying OCP before a second variant exists.

    Fix: Refactor to an extension point when change actually repeats.

13

Interview questions

How do you apply OCP to a growing switch statement?

Extract an interface for the varying behavior, move each case into its own implementation, and have the caller iterate or look up implementations; new cases become new classes.

Is it possible to be fully closed for modification?

No. You choose which axes of change to close against, based on what actually varies. Other kinds of change still require edits.

14

Practice problems

ProblemDifficultyWhat it trains
Refactor a shipping-cost switchEasyExtract interface.
Design an extensible discount engineMediumRule composition.