Open/Closed Principle (OCP)

7 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Open/Closed Principle (OCP)

1. What Is It?

The Open/Closed Principle states that software entities (classes, modules, functions) should be open for extension, but closed for modification. Originally formulated by Bertrand Meyer (1988) and popularized by Robert C. Martin, it means you should be able to add new behavior to a system without changing the existing, tested code.

Without OCP, adding a new feature requires editing existing classes. Every edit is a risk: you may introduce bugs, break existing tests, or require a new round of regression testing. OCP drives you toward a design where new behavior is added by writing new code — new subclasses, new implementations, new decorators — rather than cracking open existing files. The existing code becomes a stable foundation, not a target for modification.


QUICK CHECK

A team maintains a payment processing service that currently handles credit card payments. They need to add support for PayPal. Which approach best aligns with the Open/Closed Principle?

Choose one answer

2. How It Works

The core mechanic: identify what varies in your system and encapsulate that variation behind an . Callers depend on the ; new variants are added by implementing the abstraction, not by modifying the caller.

The classic example is a shape-area calculator. A naive implementation uses a series of if/elif checks on shape type — adding a new shape requires modifying the calculator. The OCP version defines a Shape abstraction; the calculator delegates to it, and new shapes are added without touching the calculator.

Mermaid Class Diagram

Python Example

from abc import ABC, abstractmethod
from typing import List


# BEFORE — OCP violation: calculator must be modified for every new shape
class AreaCalculatorBad:
    def total_area(self, shapes: list) -> float:
        total = 0.0
        for shape in shapes:
            if shape["type"] == "circle":
                import math
                total += math.pi * shape["radius"] ** 2
            elif shape["type"] == "rectangle":
                total += shape["width"] * shape["height"]
            # Adding "triangle" requires modifying this method
        return total


# AFTER — OCP applied: calculator is closed to modification, open to extension

class Shape(ABC):
    """Abstraction that makes the calculator closed for modification."""

    @abstractmethod
    def area(self) -> float:
        ...


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height


class Triangle(Shape):
    """New shape added WITHOUT touching AreaCalculator."""

    def __init__(self, base: float, height: float) -> None:
        self.base = base
        self.height = height

    def area(self) -> float:
        return 0.5 * self.base * self.height


class AreaCalculator:
    """Closed for modification: never needs to change when new shapes are added."""

    def total_area(self, shapes: List[Shape]) -> float:
        return sum(shape.area() for shape in shapes)


# Usage
shapes: List[Shape] = [Circle(5), Rectangle(4, 3), Triangle(6, 8)]
calculator = AreaCalculator()
print(calculator.total_area(shapes))  # Works with any Shape subclass

Java Example

import java.util.List;

// BEFORE — OCP violation
public class AreaCalculatorBad {
    public double totalArea(List<Object> shapes) {
        double total = 0;
        for (Object shape : shapes) {
            if (shape instanceof Circle c) {
                total += Math.PI * c.radius * c.radius;
            } else if (shape instanceof Rectangle r) {
                total += r.width * r.height;
            }
            // Must modify here to add Triangle
        }
        return total;
    }
}

// AFTER — OCP applied

public interface Shape {
    double area();
}

public class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle implements Shape {
    private final double width;
    private final double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

// New shape added WITHOUT modifying AreaCalculator
public class Triangle implements Shape {
    private final double base;
    private final double height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public double area() {
        return 0.5 * base * height;
    }
}

// Closed for modification
public class AreaCalculator {
    public double totalArea(List<Shape> shapes) {
        return shapes.stream()
                     .mapToDouble(Shape::area)
                     .sum();
    }
}

QUICK CHECK

A payment processing service currently handles two payment methods using if/elif checks: if method == 'credit_card' and elif method == 'paypal'. A new crypto payment method needs to be added. Which approach correctly applies the Open-Closed Principle?

Choose one answer

3. Variants & Comparisons

OCP is achieved through different mechanisms depending on language and context:

ApproachHow It WorksProsConsBest For
Inheritance / Abstract ClassOverride behavior in subclassesNatural for "is-a" hierarchiesFragile base class problem; tight couplingWell-defined taxonomies (shapes, animals)
Interface / ProtocolImplement new variant as new classLoose coupling, testableRequires upfront abstraction designMost production OOP code
Strategy PatternInject varying behavior as an objectRuntime-swappableSlightly more indirectionAlgorithms, pricing, sorting
Decorator PatternWrap existing objects to add behaviorComposable, avoids subclass explosionOrdering matters; harder to debugFeature toggles, middleware, logging
Plugin / ConfigurationRegister new behavior via configZero code changes for new variantsComplex registration infrastructureFrameworks, IDEs, CMS plugins

QUICK CHECK

A team is building an e-commerce platform where discount calculations need to switch between different pricing algorithms at runtime — for example, switching from a flat-rate discount to a percentage-based one based on user tier. Which Open-Closed Principle mechanism is best suited for this requirement?

Choose one answer

4. When to Use It (and When NOT To)

Use OCP when:

  • A class has an if/elif chain or switch that must grow every time a new variant is added
  • You have a core algorithm that different use cases must customize (pricing, validation, export format)
  • You're building a library or framework where users must extend behavior without forking your code

Anti-patterns:

  • Premature : Extracting an for behavior that only ever has one implementation. Wait for the second variant before abstracting.
  • Violating OCP in the : Making the base class area() method contain default logic with conditionals — the abstraction itself is now closed to extension.
  • Over-engineering with plugins: Building a plugin registry for behavior that changes once a year. OCP is a trade-off — the extension infrastructure has a cost.

Decision triggers:

  • "If you see a growing if shape_type == 'X' chain — reach for OCP via an abstraction."
  • "If adding a new feature means editing 5 existing files — your design is not closed for modification."

QUICK CHECK

A developer is building an internal admin tool where report exports are handled by a single function with an if/elif chain that checks the format type ('csv', 'pdf', 'excel'). A new format is requested once every 18 months, and each addition takes about 30 minutes. The developer proposes refactoring to a plugin registry with a base class and registered exporters. What is the most accurate assessment of this proposal?

Choose one answer

5. Real-World Usage

Java's Comparator : Collections.sort() is closed for modification — it never changes. New sort orders are added by implementing Comparator<T>. The entire Java sort infrastructure follows OCP.

Django's middleware: Django's request/response pipeline is a chain of middleware objects. Adding new behavior (logging, authentication, rate limiting) means adding a new middleware class — the core WSGIHandler is never modified.

JUnit's @Extension / TestRule: JUnit's test runner is closed for modification. New test lifecycle behaviors (test retries, custom logging, database setup) are added by implementing TestRule or @Extension, not by modifying the runner.


QUICK CHECK

Your team wants to add rate limiting to a Django web application. Following the Open-Closed Principle, which approach best reflects how Django's request/response pipeline is designed to be extended?

Choose one answer

6. Interview Cheat Sheet

Key sentences to say:

  1. "OCP means once a class is tested and shipped, I shouldn't need to crack it open to add new behavior — I extend it instead."
  2. "The mechanism is always the same: identify what varies, put it behind an , and add new variants by implementing the ."
  3. "The cost of OCP is the upfront abstraction. I apply it when I see the second use case — not the first."
  4. "OCP and the pattern are closely related — is the runtime mechanism that makes a class open for extension."

Common follow-up questions:

"Is OCP always achievable?"

No — some changes are inherently cross-cutting and require touching existing code. OCP is a design goal, not a law. The goal is to minimize modifications to stable, tested code.

"How do you decide when to abstract?"

The 'Rule of Three': on the first use case, implement it directly. On the second, consider abstracting. On the third, definitely abstract. Don't prematurely design for extensibility that may never be needed.

"What's the difference between OCP and just using interfaces?"

Interfaces are the mechanism; OCP is the principle. You can have interfaces everywhere and still violate OCP if you're constantly modifying the classes that depend on those interfaces.

Connections to other concepts:

  • OCP is achieved through — without , there's no way to extend behavior without modifying callers
  • Strategy Pattern is the most direct runtime implementation of OCP
  • Pattern extends behavior by wrapping, which is OCP without
  • Principle enables OCP — when high-level modules depend on abstractions, adding new low-level variants doesn't require touching the high-level code
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.