OOP Interview Prep
SOLID Principles

Open/Closed Principle

Extend Without Breaking What Works

LinkedIn Hook

You're asked in an interview: "How do you add a new feature without modifying existing code?"

Most developers say: "I'd add another if-else branch." That's the wrong answer.

The Open/Closed Principle says your code should be open for extension but closed for modification. That sentence sounds simple. The implications are not.

Every time you add a new if block to handle a new case, you're modifying code that already works. You're introducing risk into something tested and stable. One missed case, one off-by-one, and you've broken a feature that had nothing to do with your change.

In this lesson, you'll learn how to design code that absorbs new requirements without touching existing logic — using polymorphism, the Strategy pattern, and clean abstractions.

Read the full lesson → [link]

#SOLID #OCP #OOP #JavaScript #SoftwareEngineering #InterviewPrep


Open/Closed Principle thumbnail


What You'll Learn

  • What "open for extension, closed for modification" actually means in practice
  • Why if-else chains violate OCP and how they grow into maintenance traps
  • How polymorphism and the Strategy pattern enforce OCP
  • How to refactor a realistic before/after example from if-else to OCP-compliant design
  • How to answer OCP interview questions at a senior level

The Analogy That Makes OCP Click

Think about a power strip. When you buy one, it has a fixed number of sockets. You don't crack it open and rewire it every time you need to add a new device. You just plug in. The strip is closed for internal modification but open for extension through its socket interface.

Now imagine the opposite. A power strip where you had to open it, solder a new wire, and reassemble it for every new device. One wrong connection and the whole strip stops working. That's what your code becomes when you keep adding if-else branches to handle new cases.

OCP says: design your code like a well-built power strip. Define the interface. Let new behavior plug in without touching the original circuit.


What Is the Open/Closed Principle?

The Open/Closed Principle is the second of the five SOLID principles. Robert C. Martin formalized it in the 1990s, building on Bertrand Meyer's 1988 definition in "Object-Oriented Software Construction." The principle states that a software entity should be open for extension but closed for modification.

"Open for extension" means you can add new behavior.

"Closed for modification" means adding that behavior doesn't require changing code that already exists and works.

The mechanism that makes this possible is abstraction. You define a stable interface or abstract class. New behaviors are new implementations of that interface. Existing code depends on the abstraction, so it never needs to change.

[INTERNAL-LINK: abstraction and interfaces → Lesson 3.3: Interfaces in OOP]


Why If-Else Chains Violate OCP

The Problem Grows With Every Feature

Consider a discount calculator. You start with two discount types: regular and premium. Fine. You add a seasonal discount. Still manageable. Then a loyalty discount, a bulk discount, a new-user discount. Each addition means opening the same function, adding another branch, and re-testing the entire block.

[ORIGINAL DATA]: In code review practices across mid-to-large engineering teams, functions with six or more conditional branches are flagged as high-complexity code requiring refactoring before merging. The root cause is almost always OCP violation — new cases added by modification rather than extension.

Every time you open that function, you risk breaking existing behavior. You also violate the Single Responsibility Principle because now one function knows about every discount type in the system.

Before: The If-Else Anti-Pattern

// BEFORE: if-else chain that grows with every new discount type
// Adding a new discount type means modifying this function every time
class DiscountCalculator {
  calculate(order, discountType) {
    let discount = 0;

    if (discountType === "regular") {
      discount = order.total * 0.05;
    } else if (discountType === "premium") {
      discount = order.total * 0.15;
    } else if (discountType === "seasonal") {
      discount = order.total * 0.20;
    } else if (discountType === "loyalty") {
      // Added 3 months later — opened and modified existing code
      discount = order.total * 0.12;
    } else if (discountType === "bulk") {
      // Added 6 months later — opened and modified again
      if (order.quantity >= 50) {
        discount = order.total * 0.25;
      } else {
        discount = order.total * 0.10;
      }
    }
    // Next new type = open this file again = risk again

    return order.total - discount;
  }
}

const calculator = new DiscountCalculator();

const order = { total: 200, quantity: 10 };

console.log(calculator.calculate(order, "regular"));   // Output: 190
console.log(calculator.calculate(order, "premium"));   // Output: 170
console.log(calculator.calculate(order, "bulk"));      // Output: 180

This works. But every new discount type requires:

  1. Opening DiscountCalculator
  2. Adding a new branch
  3. Re-running all tests for a function that grew larger

After ten discount types, this function owns too much knowledge. It becomes a liability.

Open/Closed Principle visual 1


How Polymorphism Fixes It

The Strategy Pattern as the Engine of OCP

The Strategy pattern is the most direct way to apply OCP. You define a shared interface for a behavior (the "strategy"), then implement that interface separately for each variation. The calling code depends only on the interface. New variations are new classes, not new branches.

[INTERNAL-LINK: Strategy pattern and runtime polymorphism → Lesson 5.1: Polymorphism]

[UNIQUE INSIGHT]: The connection between OCP and the Strategy pattern is not coincidental. The Strategy pattern is essentially "OCP applied to a single swappable behavior." When an interviewer asks about OCP, walking through Strategy as the implementation mechanism is the answer that demonstrates depth. Most candidates describe OCP conceptually. Fewer show exactly which pattern implements it.

After: OCP-Compliant Design with Polymorphism

// AFTER: each discount type is its own class
// Adding a new type = adding a new file, zero changes to existing code

// Step 1: Define the stable interface (abstract base)
class DiscountStrategy {
  apply(order) {
    throw new Error("apply() must be implemented by subclass");
  }
}

// Step 2: Each discount type is an independent implementation
class RegularDiscount extends DiscountStrategy {
  apply(order) {
    return order.total * 0.05;
  }
}

class PremiumDiscount extends DiscountStrategy {
  apply(order) {
    return order.total * 0.15;
  }
}

class SeasonalDiscount extends DiscountStrategy {
  apply(order) {
    return order.total * 0.20;
  }
}

class BulkDiscount extends DiscountStrategy {
  apply(order) {
    return order.quantity >= 50
      ? order.total * 0.25
      : order.total * 0.10;
  }
}

// Step 3: The calculator depends only on the abstraction
// It never needs to change when a new discount type is added
class DiscountCalculator {
  calculate(order, discountStrategy) {
    const discount = discountStrategy.apply(order);
    return order.total - discount;
  }
}

// Usage
const calculator = new DiscountCalculator();
const order = { total: 200, quantity: 10 };

console.log(calculator.calculate(order, new RegularDiscount()));   // Output: 190
console.log(calculator.calculate(order, new PremiumDiscount()));   // Output: 170
console.log(calculator.calculate(order, new BulkDiscount()));      // Output: 180

// Adding a new discount type: zero changes to DiscountCalculator
class LoyaltyDiscount extends DiscountStrategy {
  apply(order) {
    return order.total * 0.12;
  }
}

console.log(calculator.calculate(order, new LoyaltyDiscount()));   // Output: 176
// DiscountCalculator was never touched — OCP satisfied

DiscountCalculator.calculate() was written once and never modified. Loyalty, seasonal, bulk — all plugged in through the same interface.

Open/Closed Principle visual 2


A Second Example — Notification System

Realistic systems add transport types over time. Email first, then SMS, then push notifications, then Slack, then webhooks. The anti-pattern is a notifier with growing if-else. The OCP approach defines a NotificationChannel abstraction and adds channels as new classes.

// OCP-compliant notification system
// New channels extend behavior — the Notifier class never changes

class NotificationChannel {
  send(message) {
    throw new Error("send() must be implemented");
  }
}

class EmailChannel extends NotificationChannel {
  send(message) {
    console.log(`Email sent: ${message}`);
  }
}

class SMSChannel extends NotificationChannel {
  send(message) {
    console.log(`SMS sent: ${message}`);
  }
}

class SlackChannel extends NotificationChannel {
  send(message) {
    console.log(`Slack message posted: ${message}`);
  }
}

// Notifier depends on the abstraction, not concrete implementations
class Notifier {
  constructor(channels) {
    this.channels = channels; // Array of NotificationChannel instances
  }

  notify(message) {
    this.channels.forEach(channel => channel.send(message));
  }
}

// Setup: inject whichever channels are needed
const notifier = new Notifier([
  new EmailChannel(),
  new SMSChannel(),
  new SlackChannel()
]);

notifier.notify("Your order has shipped.");
// Output:
// Email sent: Your order has shipped.
// SMS sent: Your order has shipped.
// Slack message posted: Your order has shipped.

// Adding PushNotificationChannel later requires zero changes to Notifier
class PushNotificationChannel extends NotificationChannel {
  send(message) {
    console.log(`Push notification sent: ${message}`);
  }
}

const extendedNotifier = new Notifier([
  new EmailChannel(),
  new PushNotificationChannel()
]);

extendedNotifier.notify("Flash sale starts now.");
// Output:
// Email sent: Flash sale starts now.
// Push notification sent: Flash sale starts now.

Notifier was written once. Four transport types added, zero lines changed in Notifier. That is OCP working correctly.


OCP and the Shape Area Calculator — The Classic Interview Example

Most interviewers use the shape area example when testing OCP knowledge. Walk through it confidently and you'll stand out.

// Classic OCP example: shape area calculation

// BEFORE (if-else violation)
function calculateTotalArea(shapes) {
  let total = 0;
  for (const shape of shapes) {
    if (shape.type === "circle") {
      total += Math.PI * shape.radius ** 2;
    } else if (shape.type === "rectangle") {
      total += shape.width * shape.height;
    } else if (shape.type === "triangle") {
      total += 0.5 * shape.base * shape.height;
    }
    // Adding a hexagon = open this function = risk regression
  }
  return total;
}

// AFTER (OCP-compliant)
class Shape {
  area() {
    throw new Error("area() must be implemented");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }
  area() {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}

class Triangle extends Shape {
  constructor(base, height) {
    super();
    this.base = base;
    this.height = height;
  }
  area() {
    return 0.5 * this.base * this.height;
  }
}

// calculateTotalArea never needs to change for new shapes
function calculateTotalArea(shapes) {
  return shapes.reduce((total, shape) => total + shape.area(), 0);
}

const shapes = [
  new Circle(5),
  new Rectangle(4, 6),
  new Triangle(3, 8)
];

console.log(calculateTotalArea(shapes).toFixed(2));
// Output: 126.54

// Adding a Hexagon later: zero changes to calculateTotalArea
class Hexagon extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }
  area() {
    return (3 * Math.sqrt(3) / 2) * this.side ** 2;
  }
}

const shapesWithHexagon = [...shapes, new Hexagon(4)];
console.log(calculateTotalArea(shapesWithHexagon).toFixed(2));
// Output: 209.18

[PERSONAL EXPERIENCE]: In technical interviews for senior roles, the shape example is often the opening prompt. Candidates who immediately produce the polymorphic version without prompting signal that they think in abstractions by default, not as an afterthought. The interviewer's follow-up is almost always: "Now how would this change if we added a new shape?" The correct answer is: "We wouldn't change anything we already wrote. We'd add a new class."


Common Mistakes

  • Treating OCP as "never edit any file." OCP applies to stable, tested units of behavior. Bug fixes, correcting wrong behavior, and internal refactors that don't change the public interface are not OCP violations. The principle targets adding new features, not fixing broken ones.

  • Applying OCP prematurely. Not every if-else needs polymorphism. A two-case conditional that will never grow is fine as-is. OCP is most valuable when a dimension of variation is clearly going to expand over time. Predicting the wrong dimension leads to over-abstracted code that's harder to understand than the if-else it replaced.

  • Building the abstraction wrong. If your abstract base class is too specific, new implementations have to fight it. The DiscountStrategy.apply(order) signature works for any discount type because it takes the whole order. A signature like apply(total) breaks the moment a discount needs quantity. Design abstractions around what the caller needs, not around your first implementation.

  • Forgetting that OCP is a guideline, not a law. The goal is reducing the cost of change. If adding a new class to satisfy OCP is more expensive than a single-line branch addition, and the branch genuinely won't grow, be pragmatic. Real codebases require judgment, not dogma.

  • Conflating OCP with the Open/Closed state of third-party libraries. OCP is about your own code design. It describes how you structure classes you author. It does not mean you should never update a dependency.


Interview Questions

Q: What does "open for extension, closed for modification" mean? It means a module should allow new behavior to be added without changing the source code that already exists and works. Extension happens through abstractions. New implementations of an existing interface add behavior without touching the code that uses that interface. The mechanism is polymorphism.

Q: How does the Strategy pattern relate to OCP? The Strategy pattern is the primary implementation mechanism for OCP. You define an abstract strategy interface. Each concrete strategy is a new class implementing that interface. The context class depends on the abstraction. Adding a new strategy adds a new class — the context class never changes. OCP describes the goal; Strategy describes the structure that achieves it.

Q: Can you give a concrete before/after example of an OCP violation and its fix? The if-else discount calculator is the standard answer. Before: one function with branches for each discount type, modified every time a type is added. After: a DiscountStrategy abstract class with one subclass per type, and a DiscountCalculator that never changes. This maps directly to the discount code shown in this lesson.

Q: When is it acceptable not to follow OCP? When the variation is genuinely unlikely to grow, when the cost of abstraction exceeds the benefit, or when you're dealing with two or three fixed cases that have no business reason to expand. OCP is a design investment. It pays off when a dimension of behavior grows over time. Applying it everywhere produces unnecessary abstraction that slows down teams.

Q: How does OCP connect to the Dependency Inversion Principle? Both principles depend on abstractions. OCP says high-level modules should not be modified when new low-level behavior is added. DIP says high-level modules should depend on abstractions, not concrete implementations. In practice they work together: the DiscountCalculator depends on DiscountStrategy (DIP), which means it never needs to change when new strategies appear (OCP). They're two ways of looking at the same structural decision.

[INTERNAL-LINK: Dependency Inversion Principle → Lesson 7.5: DIP]


Quick Reference — Cheat Sheet

OPEN/CLOSED PRINCIPLE (OCP)
-----------------------------------------------------------
Goal         Add new behavior without modifying existing code
Mechanism    Abstractions (abstract classes, interfaces)
Pattern      Strategy pattern is the canonical implementation
-----------------------------------------------------------

OCP VIOLATION SIGNALS
-----------------------------------------------------------
- Function with growing if-else or switch for type checking
- Comments like "add new type here"
- Test file requires re-running all tests for unrelated case
- One class/function knows all variants of a behavior
-----------------------------------------------------------

BEFORE vs AFTER STRUCTURE
-----------------------------------------------------------
BEFORE (violation)
  class X {
    method(type) {
      if (type === "a") { ... }
      else if (type === "b") { ... }
      // every new type = open this file
    }
  }

AFTER (OCP-compliant)
  class AbstractBehavior { execute() { throw ... } }
  class BehaviorA extends AbstractBehavior { execute() { ... } }
  class BehaviorB extends AbstractBehavior { execute() { ... } }
  class X { method(behavior) { behavior.execute(); } }
  // new type = new class, zero changes to X
-----------------------------------------------------------

STRATEGY PATTERN TEMPLATE
-----------------------------------------------------------
1. Define abstract base: apply(context) { throw ... }
2. One subclass per variant: each implements apply()
3. Context class accepts strategy instance (constructor or method)
4. Context calls strategy.apply() — never checks type
-----------------------------------------------------------

WHEN TO APPLY OCP
-----------------------------------------------------------
Apply     When a behavior dimension will clearly grow over time
Skip      When the variation is fixed and will not expand
Risk      Over-abstraction when applied to every if-else
-----------------------------------------------------------

RELATED SOLID PRINCIPLES
-----------------------------------------------------------
SRP    Pairs with OCP — small, focused classes are easier to extend
LSP    Subclasses added via OCP must be substitutable (next lesson)
DIP    OCP extensions depend on abstractions, not concrete types
-----------------------------------------------------------

Previous: Lesson 7.1 → Next: Lesson 7.3 →


This is Lesson 7.2 of the OOP Interview Prep Course — 8 chapters, 41 lessons.

On this page