Loading episodes…
0:00 0:00

Stop Modifying Your Code: A Visual Guide to the Open/Closed Principle

00:00
BACK TO HOME

Stop Modifying Your Code: A Visual Guide to the Open/Closed Principle

10xTeam December 16, 2025 9 min read

You’ve just shipped a critical feature. It’s tested, stable, and running perfectly in production. A week later, a new business requirement comes in that needs to alter the logic of that very feature. What do you do? If your first instinct is to open up that battle-tested class and start changing it, you might be introducing unnecessary risk.

This is where the second principle of SOLID comes into play: the Open/Closed Principle (OCP).

The Open/Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

In simple terms, you should be able to add new functionality without changing existing code. This minimizes the risk of breaking something that already works and has been proven stable.

Imagine building a house. A house that violates OCP is built like a sealed unit. To add a new floor, you’d have to demolish the roof, risking structural damage to the entire building.

A house that follows OCP, however, is designed with future expansion in mind. The roof is flat and reinforced, ready to serve as the foundation for a new floor. You can extend the house upwards without ever touching the existing, lived-in floors.

graph TD;
    subgraph Violation of OCP
        A[House v1] -- Demolish Roof --> B(Structural Risk);
        B -- Rebuild --> C[House v2 with new floor];
    end

    subgraph Adherence to OCP
        D[House v1 with Extension Point] -- Add New Floor --> E[House v2];
        D -- Remains Untouched --> D;
    end

Let’s see how this applies to code.

The Problem: A Service That Requires Modification

Imagine we’re building a DiscountService for an e-commerce platform. The initial requirement is to provide discounts for two types of customers: “Regular” (5% discount) and “VIP” (10% discount).

A quick and dirty implementation might look like this:

// src/main/java/com/example/DiscountService.java

public class DiscountService {

    public double calculateDiscount(double total, String customerType) {
        if ("REGULAR".equalsIgnoreCase(customerType)) {
            return total * 0.05; // 5% discount
        } else if ("VIP".equalsIgnoreCase(customerType)) {
            return total * 0.10; // 10% discount
        }
        return 0;
    }
}

This code works perfectly for the current requirements. We test it, deploy it, and everything is fine.

But then, the business decides to introduce two new customer tiers: “Premium” (15% discount) and “Enterprise” (20% discount).

To implement this, our only option is to modify the calculateDiscount method:

// src/main/java/com/example/DiscountService.java

public class DiscountService {

    public double calculateDiscount(double total, String customerType) {
        if ("REGULAR".equalsIgnoreCase(customerType)) {
            return total * 0.05;
        } else if ("VIP".equalsIgnoreCase(customerType)) {
            return total * 0.10;
+       } else if ("PREMIUM".equalsIgnoreCase(customerType)) {
+           return total * 0.15; // 15% discount
+       } else if ("ENTERPRISE".equalsIgnoreCase(customerType)) {
+           return total * 0.20; // 20% discount
        }
        return 0;
    }
}

[!WARNING] By modifying a class that was already tested and in production, we’ve created a risk. A small mistake in the new else if blocks could potentially break the discount calculation for all customer types, even the ones that were working before. This is called a regression bug.

This design is closed for extension (we can’t add new discount types without changing the code) and open for modification (our only path forward is to change it). This is the exact opposite of what the Open/Closed Principle prescribes.

The Solution: Designing for Extension with the Strategy Pattern

To refactor this to comply with OCP, we can use a powerful design pattern: the Strategy Pattern. The core idea is to define a family of algorithms, encapsulate each one, and make them interchangeable.

Here’s our plan:

  1. Create an Abstraction: Define an interface that represents the “contract” for any discount strategy.
  2. Implement Concrete Strategies: Create separate classes for each discount type, with each class implementing the common interface.
  3. Use the Abstraction: The DiscountService will no longer contain if/else logic. Instead, it will use a discount strategy object to perform the calculation.

Let’s see the code.

Step 1: Define the Strategy Interface

First, we create an interface that all our discount calculators will adhere to.

// src/main/java/com/example/strategy/DiscountStrategy.java

public interface DiscountStrategy {
    double applyDiscount(double total);
}

This interface defines a single method, applyDiscount, which takes the total price and returns the discounted amount.

Step 2: Create Concrete Implementations

Next, we create a separate class for each of our existing discount types.

// src/main/java/com/example/strategy/RegularDiscount.java

public class RegularDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double total) {
        return total * 0.05; // 5% discount
    }
}
// src/main/java/com/example/strategy/VipDiscount.java

public class VipDiscount implements DiscountStrategy {
    @Override
    public double applyDiscount(double total) {
        return total * 0.10; // 10% discount
    }
}

Step 3: Refactor the Service to Use the Strategy

Now, we can refactor our DiscountService. Instead of containing the logic itself, it will be given a DiscountStrategy object and will simply use it. This is a form of Dependency Injection.

// src/main/java/com/example/DiscountService.java

import com.example.strategy.DiscountStrategy;

public class DiscountService {
    private final DiscountStrategy discountStrategy;

    // The strategy is injected via the constructor
    public DiscountService(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double calculateDiscount(double total) {
        // The service delegates the calculation to the strategy object.
        // It doesn't know or care about the specific logic.
        return discountStrategy.applyDiscount(total);
    }
}

Our new design is much more robust and follows OCP. Here is a visual representation of the new structure:

classDiagram
    class DiscountService {
        -DiscountStrategy strategy
        +calculateDiscount(total)
    }
    class DiscountStrategy {
        <<interface>>
        +applyDiscount(total)
    }
    class RegularDiscount {
        +applyDiscount(total)
    }
    class VipDiscount {
        +applyDiscount(total)
    }

    DiscountService o-- DiscountStrategy
    DiscountStrategy <|.. RegularDiscount
    DiscountStrategy <|.. VipDiscount

The Payoff: Adding New Features Without Fear

Now, let’s revisit the new business requirement: add “Premium” and “Enterprise” discounts.

With our new design, this is incredibly simple and, more importantly, safe. We don’t touch the DiscountService or any of the existing, tested strategy classes.

We simply create new classes that implement the DiscountStrategy interface.

src/main/java/com/example/strategy/
├── DiscountStrategy.java
├── RegularDiscount.java
├── VipDiscount.java
├── PremiumDiscount.java   <-- NEW
└── EnterpriseDiscount.java  <-- NEW

Here is the code for the new PremiumDiscount:

// No changes to existing files. We only add a new file.
// src/main/java/com/example/strategy/PremiumDiscount.java

+ public class PremiumDiscount implements DiscountStrategy {
+     @Override
+     public double applyDiscount(double total) {
+         return total * 0.15; // 15% discount
+     }
+ }

Now, when we need to calculate a discount, we just instantiate the DiscountService with the appropriate strategy:

// Main application logic
public static void main(String[] args) {
    double orderTotal = 1000.0;

    // Calculate discount for a VIP customer
    DiscountService vipService = new DiscountService(new VipDiscount());
    double vipDiscount = vipService.calculateDiscount(orderTotal);
    System.out.println("VIP Discount: " + vipDiscount); // Prints: VIP Discount: 100.0

    // Calculate discount for a new Premium customer
    DiscountService premiumService = new DiscountService(new PremiumDiscount());
    double premiumDiscount = premiumService.calculateDiscount(orderTotal);
    System.out.println("Premium Discount: " + premiumDiscount); // Prints: Premium Discount: 150.0
}

We have successfully added new functionality without modifying any existing code. Our DiscountService is now open for extension but closed for modification.

Summary and Best Practices

mindmap
  root((Open/Closed Principle))
    Open for Extension
     
   
  ::icon(fa fa-plus-circle)
      Adding new features
      Via interfaces & polymorphism
      "Example: Strategy Pattern"
      No risk to existing code
    Closed for Modification
     
   
  ::icon(fa fa-lock)
      Existing, tested code is sacred
      Prevents regression bugs
      Improves stability & maintainability
      Achieved through abstraction

[!TIP] Best Practices for OCP:

  • Abstraction is Key: Use interfaces and abstract classes to define contracts that can be extended.
  • Dependency Injection: Pass dependencies (like our DiscountStrategy) into classes rather than having them create instances themselves.
  • Think Ahead: When designing a class, consider what aspects are most likely to change in the future. Those are prime candidates for abstraction.
Quiz: Test Your Knowledge

A new requirement has come in to add a "Student" discount of 25%. Based on the OCP-compliant design we just created, what is the correct course of action?

  1. Modify the DiscountService to add a new constructor.
  2. Create a new file named StudentDiscount.java that implements the DiscountStrategy interface.
  3. Add an else if block to one of the existing strategy classes.

Show Answer

Correct Answer: 2. The correct approach is to create a new StudentDiscount.java class. This extends the system's functionality without modifying any existing code, perfectly adhering to the Open/Closed Principle.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?