Open-Closed Principle (OCP): Extend, Don't Modify

Updated 2026-03-11

TL;DR

The Open/Closed Principle (OCP) states that software entities should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing, tested code. Following OCP reduces bugs, improves maintainability, and makes your codebase more flexible.

Prerequisites: Understanding of classes and inheritance, basic polymorphism concepts, familiarity with abstract classes or interfaces, and knowledge of method overriding.

After this topic: Identify violations of the Open/Closed Principle in existing code, refactor code to follow OCP using inheritance and polymorphism, design extensible systems that accommodate new requirements without modifying existing classes, and explain the trade-offs between flexibility and simplicity in system design.

Core Concept

What is the Open/Closed Principle?

The Open/Closed Principle (OCP) is the second principle in SOLID design principles, coined by Bertrand Meyer in 1988. It states: “Software entities (classes, modules, functions) should be open for extension but closed for modification.”

Open for extension means you can add new behavior or functionality to meet new requirements. Closed for modification means you shouldn’t change existing source code that already works and has been tested.

Why Does OCP Matter?

When you modify existing code to add features, you risk:

  • Introducing bugs into previously working functionality
  • Breaking dependent code that relies on the original behavior
  • Requiring extensive retesting of the entire module
  • Creating merge conflicts in team environments

OCP encourages designing systems where new features are added through new code, not by editing old code.

How to Achieve OCP

The primary mechanisms for following OCP are:

  1. Abstraction: Define abstract base classes or interfaces that establish contracts
  2. Polymorphism: Allow different implementations to be substituted without changing client code
  3. Inheritance: Create new classes that extend base functionality
  4. Composition: Inject dependencies that can be swapped with new implementations

The Core Trade-off

OCP requires upfront design thinking. You must identify variation points — places where requirements might change. Over-engineering for flexibility you don’t need creates unnecessary complexity. Under-engineering means you’ll modify existing code frequently. The skill is predicting which parts of your system will change and designing appropriate abstractions for those areas.

Real-World Analogy

Think of a power outlet. It’s closed for modification — you don’t rewire your house for each new device. But it’s open for extension — you can plug in any device that follows the standard interface. The outlet provides a stable contract, and new devices extend functionality without changing the outlet.

Visual Guide

Violating OCP: Modifying Existing Code

graph TD
    A["DiscountCalculator<br/>(Original)"]
    B["DiscountCalculator<br/>(Modified for VIP)"]
    C["DiscountCalculator<br/>(Modified for Seasonal)"]
    D["DiscountCalculator<br/>(Modified for Employee)"]
    
    A -->|"Add VIP discount<br/>(modify code)"| B
    B -->|"Add Seasonal discount<br/>(modify code)"| C
    C -->|"Add Employee discount<br/>(modify code)"| D
    
    style A fill:#ffcccc
    style B fill:#ffcccc
    style C fill:#ffcccc
    style D fill:#ffcccc

Each new discount type requires modifying the DiscountCalculator class, violating OCP. Every change risks breaking existing functionality.

Following OCP: Extending Through Inheritance

graph TD
    A["<<abstract>><br/>DiscountStrategy"]
    B["RegularDiscount"]
    C["VIPDiscount"]
    D["SeasonalDiscount"]
    E["EmployeeDiscount"]
    
    A -.->|extends| B
    A -.->|extends| C
    A -.->|extends| D
    A -.->|extends| E
    
    F["DiscountCalculator<br/>(unchanged)"]
    F -->|uses| A
    
    style A fill:#ccffcc
    style B fill:#e6ffe6
    style C fill:#e6ffe6
    style D fill:#e6ffe6
    style E fill:#e6ffe6
    style F fill:#ccffcc

New discount types extend the abstract DiscountStrategy without modifying DiscountCalculator. The calculator is closed for modification but open for extension.

Examples

Example 1: Violating OCP - Shape Area Calculator

Problem: A shape area calculator that requires modification for each new shape.

class AreaCalculator:
    def calculate_area(self, shapes):
        total_area = 0
        for shape in shapes:
            if shape['type'] == 'circle':
                total_area += 3.14 * shape['radius'] ** 2
            elif shape['type'] == 'rectangle':
                total_area += shape['width'] * shape['height']
            # What if we need to add triangle? We must modify this method!
            elif shape['type'] == 'triangle':
                total_area += 0.5 * shape['base'] * shape['height']
        return total_area

# Usage
calculator = AreaCalculator()
shapes = [
    {'type': 'circle', 'radius': 5},
    {'type': 'rectangle', 'width': 4, 'height': 6}
]
print(calculator.calculate_area(shapes))  # Output: 102.5

Why this violates OCP: Every time you add a new shape (triangle, pentagon, etc.), you must modify the calculate_area method. This means:

  • Retesting the entire AreaCalculator class
  • Risk of breaking existing shape calculations
  • Growing if-elif chain becomes unmaintainable

Example 2: Following OCP - Extensible Shape System

Solution: Use abstraction and polymorphism to make the system extensible.

from abc import ABC, abstractmethod
import math

# Abstract base class defines the contract
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

# Concrete implementations - each shape knows how to calculate its own area
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return math.pi * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

# NEW: Add triangle without modifying existing code
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height
    
    def area(self):
        return 0.5 * self.base * self.height

# AreaCalculator is now CLOSED for modification
class AreaCalculator:
    def calculate_area(self, shapes):
        return sum(shape.area() for shape in shapes)

# Usage
calculator = AreaCalculator()
shapes = [
    Circle(5),
    Rectangle(4, 6),
    Triangle(4, 3)  # New shape added without changing AreaCalculator!
]
print(f"{calculator.calculate_area(shapes):.2f}")  # Output: 108.54

Expected Output: 108.54 (78.54 + 24 + 6)

Why this follows OCP:

  • AreaCalculator never needs modification when adding new shapes
  • Each shape encapsulates its own area calculation logic
  • New shapes extend the Shape abstraction
  • Existing code remains untouched and stable

Try it yourself: Add a Pentagon class with a constructor that takes side_length. Use the formula: area = (5 * side² * √3) / 4. The calculator should work without any changes.

Example 3: Payment Processing System

Violating OCP:

class PaymentProcessor:
    def process_payment(self, amount, payment_type, details):
        if payment_type == 'credit_card':
            # Credit card processing logic
            print(f"Processing ${amount} via Credit Card: {details['card_number']}")
            return True
        elif payment_type == 'paypal':
            # PayPal processing logic
            print(f"Processing ${amount} via PayPal: {details['email']}")
            return True
        # Adding crypto requires modifying this method
        elif payment_type == 'crypto':
            print(f"Processing ${amount} via Crypto: {details['wallet']}")
            return True
        else:
            return False

# Usage
processor = PaymentProcessor()
processor.process_payment(100, 'credit_card', {'card_number': '1234-5678'})
# Output: Processing $100 via Credit Card: 1234-5678

Following OCP:

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount):
        pass

class CreditCardPayment(PaymentMethod):
    def __init__(self, card_number):
        self.card_number = card_number
    
    def process(self, amount):
        print(f"Processing ${amount} via Credit Card: {self.card_number}")
        return True

class PayPalPayment(PaymentMethod):
    def __init__(self, email):
        self.email = email
    
    def process(self, amount):
        print(f"Processing ${amount} via PayPal: {self.email}")
        return True

# NEW: Add cryptocurrency without modifying PaymentProcessor
class CryptoPayment(PaymentMethod):
    def __init__(self, wallet_address):
        self.wallet_address = wallet_address
    
    def process(self, amount):
        print(f"Processing ${amount} via Crypto: {self.wallet_address}")
        return True

# Processor is closed for modification
class PaymentProcessor:
    def process_payment(self, amount, payment_method: PaymentMethod):
        return payment_method.process(amount)

# Usage
processor = PaymentProcessor()
processor.process_payment(100, CreditCardPayment('1234-5678'))
processor.process_payment(50, CryptoPayment('0xABC123'))
# Output:
# Processing $100 via Credit Card: 1234-5678
# Processing $50 via Crypto: 0xABC123

Java/C++ Note: In Java, you’d use interface PaymentMethod instead of an abstract class (unless you need shared implementation). In C++, you’d use pure virtual functions: virtual bool process(double amount) = 0;

Try it yourself: Add a BankTransferPayment class that takes account_number and routing_number. Process a payment of $200 without modifying PaymentProcessor.

Common Mistakes

1. Over-Engineering with Premature Abstraction

Mistake: Creating complex inheritance hierarchies for every possible future change.

# Over-engineered for a simple use case
class AbstractUserNameFormatter(ABC):
    @abstractmethod
    def format(self, user): pass

class FirstNameFormatter(AbstractUserNameFormatter):
    def format(self, user): return user.first_name

class FullNameFormatter(AbstractUserNameFormatter):
    def format(self, user): return f"{user.first_name} {user.last_name}"

Why it’s wrong: If name formatting requirements are stable and unlikely to change, this abstraction adds unnecessary complexity. OCP doesn’t mean “abstract everything” — it means abstract variation points that actually vary.

Better approach: Start simple. Add abstraction when you have concrete evidence of variation (e.g., second requirement that differs from the first).

2. Modifying Base Classes Instead of Extending

Mistake: Adding new methods to abstract base classes when new requirements emerge.

class Shape(ABC):
    @abstractmethod
    def area(self): pass
    
    # Later, someone adds this, forcing all shapes to implement it
    @abstractmethod
    def perimeter(self): pass  # Violates OCP!

Why it’s wrong: Adding perimeter() to the base class means modifying every existing shape implementation. This breaks the “closed for modification” principle.

Better approach: If only some shapes need perimeter, create a separate interface/mixin or use optional methods with default implementations.

3. Using Type Checking Instead of Polymorphism

Mistake: Checking object types with isinstance() or type() instead of relying on polymorphism.

class NotificationSender:
    def send(self, notification):
        if isinstance(notification, EmailNotification):
            # Send email
            pass
        elif isinstance(notification, SMSNotification):
            # Send SMS
            pass
        # Adding push notification requires modifying this method

Why it’s wrong: This is just a disguised if-elif chain. You still need to modify NotificationSender for each new notification type.

Better approach: Define a Notification interface with a send() method. Let each notification type implement its own sending logic.

4. Confusing OCP with Never Changing Code

Mistake: Believing that following OCP means you can never modify existing classes.

Why it’s wrong: OCP is about minimizing modifications for new features, not eliminating all changes. Bug fixes, performance improvements, and refactoring are legitimate reasons to modify code. The principle targets feature additions that should be handled through extension.

Guideline: If you’re changing code because requirements changed in an unpredictable way, that’s acceptable. If you’re changing code because you’re adding a predictable variation of existing functionality, consider extension instead.

5. Ignoring the Cost of Flexibility

Mistake: Making every class extensible “just in case,” leading to over-complicated codebases.

Why it’s wrong: Abstraction has costs: more classes, more indirection, harder to understand for newcomers. If a class handles a stable, well-understood domain with low change frequency, keeping it simple is better than making it extensible.

Better approach: Apply OCP to parts of your system with high change frequency or known variation points. Use the “rule of three” — wait until you have three similar implementations before abstracting.

Interview Tips

Recognizing OCP Violations in Code Reviews

Interviewers often present code and ask you to identify design principle violations. For OCP, look for:

  • If-elif chains based on type or category
  • Switch statements on object types
  • Type checking with isinstance(), typeof, or similar
  • Frequent modifications to the same class for new features

When you spot these, say: “This violates the Open/Closed Principle because adding new [feature type] requires modifying existing code. We could refactor using [polymorphism/strategy pattern/inheritance].”

Explaining OCP in System Design Questions

When designing systems (e.g., “Design a notification system”), explicitly mention OCP:

“I’ll design the notification system to follow the Open/Closed Principle. I’ll create a Notification interface with a send() method. Each notification type — email, SMS, push — will implement this interface. The NotificationService will depend on the interface, not concrete types. This way, we can add new notification channels without modifying the service.”

This shows you think about maintainability and extensibility, not just functionality.

Discussing Trade-offs

Senior-level interviews expect you to discuss trade-offs. When asked about OCP, mention:

  • When to apply it: “I’d apply OCP to the payment processing module because payment methods frequently change. I wouldn’t apply it to the user authentication flow, which is stable.”
  • Costs: “Following OCP adds abstraction layers. For a small startup with 2 developers, over-engineering for flexibility might slow development. For a large team, it prevents merge conflicts and enables parallel work.”
  • YAGNI principle: “I balance OCP with ‘You Aren’t Gonna Need It.’ I add abstraction when I see actual variation, not hypothetical future needs.”

Common Interview Questions

Q: “How does OCP relate to other SOLID principles?”

A: “OCP works closely with Liskov Substitution Principle — you need substitutable subclasses to extend without modifying. It also relates to Dependency Inversion — depending on abstractions makes your code open for extension. Interface Segregation supports OCP by preventing fat interfaces that force modifications.”

Q: “Give an example where you applied OCP in a real project.”

A: Prepare a specific story. Example: “In a reporting system, we had a ReportGenerator with if-statements for PDF, Excel, and CSV. When we needed JSON and XML, I refactored to a ReportFormatter interface. Each format became a separate class. Adding new formats no longer touched the generator. This reduced bugs and made testing easier.”

Q: “When would you NOT follow OCP?”

A: “When the cost of abstraction exceeds the benefit. For example, a simple utility function that formats dates in one specific way doesn’t need an extensible architecture. Also, in prototypes or MVPs where requirements are unclear, premature abstraction wastes time. I’d wait for requirements to stabilize before adding flexibility.”

Key Takeaways

  • OCP means extending behavior through new code, not modifying existing tested code — use abstraction and polymorphism to add features without changing stable classes
  • Identify variation points early — apply OCP to parts of your system likely to change (payment methods, notification types, report formats), not to stable domains
  • Abstraction has costs — balance flexibility with simplicity using the “rule of three” (abstract after seeing three similar implementations)
  • Type checking is a code smell — if you’re using isinstance(), typeof, or switch statements on types, you’re likely violating OCP and should refactor to polymorphism
  • OCP enables parallel development and reduces bugs — when teams can add features by creating new classes instead of modifying shared code, you avoid merge conflicts and reduce regression risk