Question 3 · Section 18

What is Open-Closed Principle?

In simpler terms: you should be able to add new functionality to the system without changing already existing and working code.

Language versions: English Russian Ukrainian

🟢 Junior Level

Open-Closed Principle (OCP) is one of the five SOLID principles, which states: “Software entities should be open for extension, but closed for modification.

Explanation: Software entities = classes, modules, functions. “Closed for modification” = don’t change already tested and working code. Instead — add new classes/modules. “Open for extension” = new functionality can be added without modifying existing code.”.

In simpler terms: you should be able to add new functionality to the system without changing already existing and working code.

Simple analogy: Think of a smartphone with a USB port. You can connect new devices (headphones, flash drive, keyboard) without opening or modifying the phone itself.

OCP violation example:

// Bad: adding a new shape requires modifying this method
public double calculateArea(Object shape) {
    if (shape instanceof Circle) {
        Circle c = (Circle) shape;
        return Math.PI * c.radius * c.radius;
    } else if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle) shape;
        return r.width * r.height;
    }
    // New shape type — need to modify this method!
    throw new IllegalArgumentException("Unknown shape");
}

OCP-compliant example:

// Good: each class knows its own area
public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    private double radius;
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle implements Shape {
    private double width;
    private double height;
    @Override
    public double calculateArea() {
        return width * height;
    }
}

// Client code — DOES NOT CHANGE when adding new shapes
public double calculateTotalArea(List<Shape> shapes) {
    return shapes.stream().mapToDouble(Shape::calculateArea).sum();
}

When to use:

  • Always when the system is expected to expand with new types, formats, rules
  • When designing APIs, libraries, plugins
  • When creating business logic that will change over time

🟡 Middle Level

How It Works

OCP relies on two key mechanisms:

  1. Abstractions (interfaces, abstract classes): client code depends on abstraction, not on a specific implementation
  2. Polymorphism: new implementations can be substituted without changing client code

How to Detect OCP Violation

Signs of violation:

  1. Chains of if-else or switch by object type: each new type requires adding a new branch
  2. Frequent edits in stable modules: code that should be “frozen” is constantly edited
  3. Regression bugs: changes for a new feature break old functionality
  4. Raw instanceof checks — a sign of OCP violation. Exception — pattern matching with sealed interface (Java 21+), where the compiler guarantees exhaustiveness of all variants.

Pattern Matching with sealed interface is available since Java 21. For Java 8/11, use the Strategy pattern.

Practical Application

Strategy pattern for OCP:

// OCP violation: method with lots of conditions
public class PaymentService {
    public void processPayment(String type, BigDecimal amount) {
        if ("credit_card".equals(type)) {
            processCreditCard(amount);
        } else if ("paypal".equals(type)) {
            processPayPal(amount);
        } else if ("crypto".equals(type)) {
            processCrypto(amount);
        }
        // New payment method — modifying existing code!
    }
}

// OCP-compliant: strategy
public interface PaymentStrategy {
    void process(BigDecimal amount);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void process(BigDecimal amount) { /* logic */ }
}

public class PaymentService {
    private final Map<String, PaymentStrategy> strategies;

    public PaymentService(List<PaymentStrategy> strategyList) {
        this.strategies = strategyList.stream()
            .collect(Collectors.toMap(
                s -> s.getClass().getSimpleName(),
                s -> s
            ));
    }

    public void processPayment(String type, BigDecimal amount) {
        PaymentStrategy strategy = strategies.get(type);
        if (strategy == null) {
            throw new IllegalArgumentException("Unknown payment type: " + type);
        }
        strategy.process(amount);
    }
}

Common Mistakes

  1. Mistake: Creating an interface “for the future” for a class with one implementation Solution: Follow YAGNI — add abstraction when a second implementation or real need appears

  2. Mistake: Overusing inheritance instead of composition Solution: Prefer composition — it gives more flexibility

  3. Mistake: OCP at only one level (extending service, but not repository) Solution: Apply the principle consistently across the entire architecture

When NOT to Strictly Follow OCP

  • Simple CRUD applications with a fixed set of operations
  • Prototypes and MVPs (fast hypothesis validation)
  • Code that definitely won’t be extended (utilities, format converters)

🔴 Senior Level

Internal Implementation and Architecture

OCP is not just “use interfaces”. It’s a principle of managing change risks. Bertrand Meyer, who formulated OCP in 1988, meant the following:

A module is “closed” when it is available for use by other modules. It is “open” when it remains available for extension (new behaviors can be defined in it).

At the Senior level, it’s important to understand: OCP applies not to every class, but to architecturally significant extension points.

Architectural Trade-offs

Strict OCP compliance:

  • ✅ Pros: Minimum regression bugs, easy extension, stable API, parallel development
  • ❌ Cons: More abstractions, initial design complexity, cognitive overhead

Moderate OCP compliance:

  • ✅ Pros: Balance between simplicity and flexibility, less boilerplate
  • ❌ Cons: Requires mature judgment, risk of gradual technical debt accumulation

Edge Cases

  1. Abstractions Overkill: No need to create an interface for every class. If a class will have only one implementation — an interface may be unnecessary complexity (YAGNI violation)

  2. Interdependent extensions: When a new feature requires changes in 3+ layers (controller → service → repository)
    • Solution: Design “extension points” in advance at the API/contract level
  3. Inheritance vs Composition: Inheritance creates rigid coupling. Composition with delegation — flexible
    • Rule: Prefer composition, inheritance — only for true “is-a” relationships

Performance

  • Virtual method dispatch: Calling a method through an interface adds indirection. JVM performs inline caching and devirtualization, minimizing overhead
  • Startup time: A large number of small classes/interfaces slightly slows startup (metadata loading, classloading)
  • JIT optimization: The more concrete implementations of an interface, the harder it is for JIT to choose the optimal inlining strategy. With > 2-3 implementations, a megamorphic call may occur — significant performance degradation. When an interface has 3+ implementations, the JVM cannot optimize the call. Instead of a direct call (1-2 ns), a table lookup occurs (~10-20 ns).

Production Experience

Real production scenario:

In a financial project, there was a fee calculation system with hardcoded logic for 3 client types. When entering a new market (10+ countries), 6 months of refactoring were needed:

  1. Extracted FeeCalculator interface
  2. Created PercentageFeeCalculator, TieredFeeCalculator, FlatFeeCalculator
  3. Implemented a factory based on configuration

Result: Adding a new country was reduced from 2 weeks to 2 days (configuration + one new class only).

Monitoring and Diagnostics

How to detect OCP violation in code:

  1. Code metrics:
    • Number of if-else / switch branches by type > 3
    • File change frequency (> 5 edits/month in a stable module)
    • Number of instanceof checks
  2. Tools:
    • SonarQube (Cognitive Complexity, Number of Branches)
    • Git history (git log --follow -- <file> — frequent edits = OCP violation)
    • ArchUnit (architectural tests on dependencies)
  3. CI/CD signs:
    • Regression bugs when adding new features
    • Long code reviews due to large diffs in stable classes

Best Practices for Highload

  • Plugin Architecture: Design the system as a core + plugins. Core is stable, plugins extend
  • Specification Pattern: For complex business logic, use specification composition instead of if-else
  • Event-Driven Extension: Instead of direct calls, use events — new handlers subscribe without modifying the publisher

Relationship with Other Principles

  • OCP ← SRP: When a class has one responsibility, it’s easier to extend without modification
  • OCP → LSP: New implementations must correctly substitute the abstraction
  • OCP → ISP: Small interfaces are easier to extend
  • OCP → DIP: Dependency on abstractions is the foundation for OCP

Summary for Senior

  • OCP is about risk reduction, not architectural beauty
  • Apply OCP to extension points, not to every class
  • Remember about JIT megamorphic calls — too many interface implementations degrade performance
  • Use Event-Driven approach for loose coupling of extensions
  • OCP without real need for extension is over-engineering

🎯 Interview Cheat Sheet

Must know:

  • OCP: classes are open for extension, closed for modification
  • Main violation sign — chains of if-else / switch by object type
  • Strategy pattern — main tool for OCP compliance
  • OCP relies on abstractions (interfaces) and polymorphism
  • Megamorphic call: with >2-3 interface implementations, JIT loses optimization
  • OCP applies to extension points, not to every class
  • Pattern matching with sealed interface (Java 21+) — legitimate alternative to Strategy

Common follow-up questions:

  • How to detect OCP violation?instanceof checks, frequent edits in stable modules, regression bugs
  • When is OCP NOT needed? — CRUD with fixed operations, prototypes, code that won’t be extended
  • What is a megamorphic call? — When an interface has 3+ implementations, JVM cannot optimize the call (10-20 ns instead of 1-2 ns)
  • OCP and Plugin Architecture? — Core is stable, plugins extend via SPI/ServiceLoader

Red flags (DO NOT say):

  • “Need to create an interface for every class for the future” (YAGNI violation)
  • “OCP means code cannot be changed at all” (can’t modify working, but can extend)
  • “Switch is always bad” (for closed hierarchies with sealed interface — acceptable)

Related topics:

  • [[4. How to refactor code that violates Open Closed principle]]
  • [[19. How SOLID principles help when extending functionality]]
  • [[7. What is Interface Segregation principle]]
  • [[8. What is Dependency Inversion principle]]