Skip to main content
Scenario Article

How the Open/Closed Principle Enhances Software Development

The Fragile Base Class Problem

Abstract Algorithmsβ€’Mar 9, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .

🎯 System Scenario & Requirements

System Goal

Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .
  • You risk: - Breaking credit card handling.
  • - Merging conflicts from other teams editing the same file.

Constraints & Assumptions

  • That's only worth the cost when variation is genuinely expected.
  • The trap: abstracting too early.

⚑ Execution Stages & Decision Trace

1

Stage 1 β€” Open for Extension, Closed for Modification" β€” What That Actually Means

Action / Decision:

Every time you open a file to add a new feature, you risk breaking something that already works.

Component Behavior:

OCP says: instead of modifying existing code, design it so you can add new behavior by adding new code β€” not changing old code.

Observable Output: The classic metaphor: a plugin system .
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

Scenario context

Here's a payment processor that violates OCP:

class PaymentProcessor:
    def process(self, payment_type, amount):
        if payment_type == "credit_card":
            print(f"Processing credit card: ${amount}")
        elif payment_type == "paypal":
            print(f"Processing PayPal: ${amount}")
        # Every new payment method = edit THIS file

Each time you add a new payment method (Apple Pa…

This walkthrough focuses on The Fragile Base Class Problem.

Before making a design choice, clarify:

  • Here's a payment processor that violates OCP: Each time you add a new payment method (Apple Pay, Crypto), you edit PaymentProcessor.process() .
  • You risk: - Breaking credit card handling.
    • Merging conflicts from other teams editing the same file.

Concepts in play: design-principles-solid, jvm-internals-and-garbage-collection, low-level-design-systems, python-concurrency-nogil

Stage 1 β€” Open for Extension, Closed for Modification" β€” What That Actually Means

  • Every time you open a file to add a new feature, you risk breaking something that already works.
  • OCP says: instead of modifying existing code, design it so you can add new behavior by adding new code β€” not changing old code.
  • The classic metaphor: a plugin system .
  • You don't modify Firefox to add an ad-blocker.

Observable outcome: You don't modify Firefox to add an ad-blocker.

Stage 2 β€” OCP via Polymorphism: Extending Without Modifying

  • The fix: define a common abstraction and extend it.
  • PaymentProcessor is now closed for modification.
  • You can add 50 new payment methods and never touch it.
from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount: float) -> None:
        ...

class CreditCard(PaymentMethod):
    def process(self, amount):
        print(f"Processing credit card: ${amount}")

class PayPal(PaymentMethod):
    def process(self, amount):
        print(f"Processing PayPal: ${amount}")

class CryptoWallet(PaymentMethod):           # NEW β€” no existing file touched
    def process(self, amount):
        print(f"Processing crypto: ${amount}")

class PaymentProcessor:
    def process(self, method: PaymentMethod, amount: float):
        method.process(amount)               # Never changes
classDiagram
    class PaymentMethod {
        <>
        +process(amount)
    }
    PaymentMethod <|-- CreditCard
    PaymentMethod <|-- PayPal
    PaymentMethod <|-- CryptoWallet
    PaymentProcessor --> PaymentMethod

Observable outcome: You can add 50 new payment methods and never touch it.

Stage 3 β€” Deep Dive: Java Interface Implementation

  • In Java, the same pattern uses interfaces: Testing benefit: You can write a FakePayment implementing PaymentMethod for unit tests without any production code changes.
  • The Java version also demonstrates an important architectural boundary: the PaymentProcessor class lives in a separate module and has zero compile-time dependency on CreditCard , CryptoWallet , or any concrete payment t…
  • Those classes are loaded at runtime, which means you can add, remove, or swap payment providers via dependency injection without recompiling PaymentProcessor .
  • This is OCP operating at the module boundary level β€” a pattern you'll find in every major Java framework from Spring to Jakarta EE.
public interface PaymentMethod {
    void process(double amount);
}

public class CreditCard implements PaymentMethod {
    public void process(double amount) {
        System.out.printf("Credit card: $%.2f%n", amount);
    }
}

public class CryptoWallet implements PaymentMethod {   // New β€” zero changes to CreditCard
    public void process(double amount) {
        System.out.printf("Crypto: $%.2f%n", amount);
    }
}

public class PaymentProcessor {
    public void charge(PaymentMethod method, double amount) {
        method.process(amount);   // Closed β€” never modified
    }
}
classDiagram
    class Shape {
        <>
        +area() double
        +perimeter() double
    }
    class Circle {
        -double radius
        +area() double
        +perimeter() double
    }
    class Rectangle {
        -double width
        -double height
        +area() double
        +perimeter() double
    }
    class Triangle {
        -double base
        -double height
        +area() double
        +perimeter() double
    }
    class AreaCalculator {
        +totalArea(shapes) double
    }
    Shape <|.. Circle
    Shape <|.. Rectangle
    Shape <|.. Triangle
    AreaCalculator ..> Shape

Observable outcome: This is OCP operating at the module boundary level β€” a pattern you'll find in every major Java framework from Spring to Jakarta EE.

Stage 4 β€” OCP Extension Flow

  • The key insight of OCP is that adding new behavior never requires touching PaymentProcessor .
  • New payment types are added by creating new classes that plug into the existing abstraction.
  • When Apple Pay is needed, a developer creates ApplePayMethod implements PaymentMethod and passes it to PaymentProcessor .
  • Zero lines of PaymentProcessor or any existing payment class are changed β€” that is OCP in action.
flowchart LR
    A[PaymentProcessor.process] --> B[PaymentMethod interface]
    B --> C[CreditCard]
    B --> D[PayPal]
    B --> E[CryptoWallet]
    E --> F[NEW: no existing code modified]
    G[Add Apple Pay] --> H[Create new class]
    H --> B

Observable outcome: Zero lines of PaymentProcessor or any existing payment class are changed β€” that is OCP in action.

Stage 5 β€” Real-World Application: OCP in Production Systems

  • OCP is not just a class design pattern β€” it appears throughout distributed systems and product architecture.
  • Real-world example β€” VS Code extensions: VS Code defines extension APIs (the abstraction).
  • Extension authors add new functionality (language support, themes, debuggers) by implementing those APIs.
  • The VS Code core is never modified to support new extensions.

Observable outcome: The VS Code core is never modified to support new extensions.

Stage 6 β€” Hands-On: Apply OCP to a Report Generator

  • This exercise demonstrates the most common OCP violation pattern: a class that grows a new elif branch for every new output format, using ReportGenerator as the example because report formats are a natural extension poi…
  • This particular violation was chosen because it mirrors real-world code you will find in ETL pipelines, data export tools, and document services.
  • As you read through the violation code, focus on the line you would have to change to add Excel support β€” that modification point is the exact boundary OCP asks you to protect.
  • OCP-compliant refactor: Before vs After: πŸ“Š Strategy Pattern: OCP Applied to Sorting Algorithms This class diagram shows the Strategy pattern applied to sorting: Sorter holds a reference to the SortStrategy interface an…
# πŸ›‘ OCP violation β€” every new format requires modifying this class
class ReportGenerator:
    def generate(self, data, format_type: str):
        if format_type == "pdf":
            return self._render_pdf(data)
        elif format_type == "csv":
            return self._render_csv(data)
        elif format_type == "html":
            return self._render_html(data)
        # Adding Excel requires modifying this class

    def _render_pdf(self, data): ...
    def _render_csv(self, data): ...
    def _render_html(self, data): ...
from abc import ABC, abstractmethod

class ReportFormatter(ABC):
    @abstractmethod
    def format(self, data) -> str: ...

class PDFFormatter(ReportFormatter):
    def format(self, data) -> str:
        return f"[PDF] {data}"

class CSVFormatter(ReportFormatter):
    def format(self, data) -> str:
        return ",".join(str(v) for v in data)

class ExcelFormatter(ReportFormatter):   # NEW β€” zero existing code changed
    def format(self, data) -> str:
        return f"[Excel] {data}"

class ReportGenerator:
    def generate(self, data, formatter: ReportFormatter) -> str:
        return formatter.format(data)   # Never changes

Observable outcome: OCP-compliant refactor: Before vs After: πŸ“Š Strategy Pattern: OCP Applied to Sorting Algorithms This class diagram shows the Strategy pattern applied to sorting: Sorter holds a reference to the SortStrategy interface an…

End-to-end outcome

After working through the stages, you should be able to explain:

    • Apply OCP reactively, not preemptively.
  • The first time you write payment processing, write CreditCard directly.

Alternatives considered

Decision Guide

Decision Guide

Failure and degraded modes

Trade-offs & Failure Modes: When OCP Is Over-Engineering

  • Detection: That's only worth the cost when variation is genuinely expected.
  • User or operator impact: That's only worth the cost when variation is genuinely expected.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit