Chapter 02 · Article 04 of 55

Open-Closed Principle (OCP)

"Software entities (classes, modules, functions) should be open for extension, but closed for modification." - Bertrand Meyer, Object-Oriented Software Construction (1988)

Article outline14 sections on this page

"Software entities (classes, modules, functions) should be open for extension, but closed for modification."

  • Bertrand Meyer, Object-Oriented Software Construction (1988)

Overview

The Open-Closed Principle (OCP) is the second principle in the SOLID acronym and arguably the most architecturally significant. Bertrand Meyer introduced it in 1988, and Robert C. Martin later popularized it as a cornerstone of object-oriented design.

Open for extension means that the behavior of a module can be extended - you can add new functionality as requirements change.

Closed for modification means that extending behavior does not require changing the existing source code of that module.

At first glance this sounds contradictory. The resolution lies in abstraction: you define stable interfaces or abstract classes that act as fixed contracts, then introduce new behavior by creating new implementations rather than editing existing ones.

Meyer's original formulation relied on implementation inheritance. Martin's modern interpretation favors polymorphic OCP - depending on abstractions (interfaces/abstract classes) so that new concrete classes can be plugged in without touching consumer code.

The litmus test: Can you add a new feature by writing new code rather than changing existing code? If yes, your design respects OCP.


Problem It Solves

ProblemDescription
Fragile codeModifying a working class to add a feature risks introducing bugs in existing functionality
Shotgun surgeryA single new requirement forces changes across multiple files/classes
Ripple effectsOne change cascades through dependent modules, requiring retesting of the entire system
Merge conflictsMultiple developers modifying the same file to add different features
Regression riskEvery modification to stable code is a potential regression

Without OCP, adding a new payment method means editing PaymentProcessor, updating every switch statement, modifying tests, and hoping nothing breaks. With OCP, you write a new PaymentMethod implementation and register it - zero changes to existing code.


When to Use / When NOT to Use

When to UseWhen NOT to Use
Module has a history of frequent extensionsSimple CRUD with unlikely variation
Multiple developers work on the same componentPrototyping / throwaway code
You can identify a clear axis of changeOnly one or two concrete cases exist and won't grow
Plugin/provider architecturesPerformance-critical hot paths where indirection costs matter
Public APIs / libraries consumed by othersInternal utilities with a single caller
Domain has well-understood variation pointsRequirements are completely unknown (premature abstraction)

Rule of thumb: Apply OCP at the second variation. The first time you need a new type, refactor toward OCP. Don't pre-engineer abstractions for hypothetical futures (see YAGNI tension below).


Key Concepts & Theory

Abstraction as the Key Mechanism

OCP is achieved through strategic abstraction. You identify the axis of change - the dimension along which requirements will vary - and introduce an interface at that boundary.

Polymorphism

Runtime polymorphism (virtual dispatch) allows client code to operate on the abstraction while concrete implementations vary freely. The client is closed to modification because it never references concrete types.

Plugin Architecture

OCP at the architectural level produces plugin systems. The host application defines extension points (interfaces), and plugins implement them. Examples: VS Code extensions, webpack loaders, Spring beans.

Dependency Inversion Connection

OCP and the Dependency Inversion Principle (DIP) are deeply linked. DIP says "depend on abstractions, not concretions" - which is exactly the mechanism that makes OCP possible. You cannot have OCP without DIP.

The "Protected Variation" Perspective

Craig Larman calls this Protected Variations: identify points of predicted variation and create a stable interface around them to shield the rest of the system.


ASCII Class Diagram

Violation - Switch/If-Else Chain

┌─────────────────────────┐
│    AreaCalculator        │
├─────────────────────────┤
│ + calculate(shape)      │
│   if shape == "circle"  │
│     return π * r²       │
│   if shape == "rect"    │
│     return w * h        │
│   if shape == "tri"     │  ← Must MODIFY this class
│     return 0.5 * b * h  │     for every new shape
└─────────────────────────┘
         ▲
         │ depends on concrete types
┌────────┴────────┐
│  Circle, Rect,  │
│  Triangle ...   │  ← No common contract
└─────────────────┘

Correct - Polymorphic Design

        ┌──────────────────┐
        │  «interface»     │
        │     Shape        │
        ├──────────────────┤
        │ + area(): float  │
        └────────┬─────────┘
                 │ implements
       ┌─────────┼──────────┐
       │         │          │
┌──────┴───┐ ┌──┴─────┐ ┌──┴──────┐
│  Circle  │ │  Rect  │ │Triangle │  ← Add new shapes
├──────────┤ ├────────┤ ├─────────┤     WITHOUT modifying
│ + area() │ │+ area()│ │+ area() │     AreaCalculator
└──────────┘ └────────┘ └─────────┘

┌─────────────────────────┐
│    AreaCalculator        │
├─────────────────────────┤
│ + calculate(Shape s)    │
│   return s.area()       │  ← CLOSED for modification
└─────────────────────────┘

Pseudocode Implementation

BAD - If-Else Chain (Violates OCP)

class AreaCalculator:
    function totalArea(shapes[]):
        total = 0
        for shape in shapes:
            if shape.type == "circle":
                total += PI * shape.radius ^ 2
            else if shape.type == "rectangle":
                total += shape.width * shape.height
            else if shape.type == "triangle":
                total += 0.5 * shape.base * shape.height
            // Adding a new shape? MODIFY this method ↑
        return total

Problem: Every new shape requires editing totalArea(). This class is never "done."

GOOD - Polymorphic Design (Respects OCP)

interface Shape:
    function area(): float

class Circle implements Shape:
    radius: float
    function area(): return PI * radius ^ 2

class Rectangle implements Shape:
    width, height: float
    function area(): return width * height

class Triangle implements Shape:
    base, height: float
    function area(): return 0.5 * base * height

// CLOSED for modification  -  never changes
class AreaCalculator:
    function totalArea(shapes: Shape[]):
        total = 0
        for shape in shapes:
            total += shape.area()  // polymorphic dispatch
        return total

Adding a Pentagon? Write class Pentagon implements Shape - zero changes to AreaCalculator.

Strategy Pattern Connection

OCP is the principle; Strategy is a pattern that implements it:

interface DiscountStrategy:
    function apply(order): float

class SeasonalDiscount implements DiscountStrategy:
    function apply(order): return order.total * 0.10

class LoyaltyDiscount implements DiscountStrategy:
    function apply(order): return order.total * 0.15

// OrderProcessor is CLOSED  -  new discounts = new strategy classes
class OrderProcessor:
    strategy: DiscountStrategy

    function checkout(order):
        discount = strategy.apply(order)
        return order.total - discount

Real-World Examples

1. Payment Processing

interface PaymentGateway:
    function charge(amount, token): Result

class StripeGateway implements PaymentGateway: ...
class PayPalGateway implements PaymentGateway: ...
class CryptoGateway implements PaymentGateway: ...  // Added later, zero changes to checkout

class CheckoutService:
    gateway: PaymentGateway
    function processPayment(order):
        return gateway.charge(order.total, order.paymentToken)

Business impact: Adding Apple Pay means writing one new class and a config entry - no PR touching core checkout logic.

2. Notification Channels

interface NotificationChannel:
    function send(message, recipient): void

class EmailChannel implements NotificationChannel: ...
class SMSChannel implements NotificationChannel: ...
class SlackChannel implements NotificationChannel: ...  // Added in sprint 14

class NotificationService:
    channels: NotificationChannel[]
    function notify(event):
        for channel in channels:
            channel.send(event.message, event.recipient)

3. Report Generators

interface ReportExporter:
    function export(data): bytes

class PDFExporter implements ReportExporter: ...
class CSVExporter implements ReportExporter: ...
class ExcelExporter implements ReportExporter: ...  // New requirement, no modification

class ReportService:
    function generateReport(data, exporter: ReportExporter):
        return exporter.export(data)

Comparison with Similar Patterns

AspectOCPStrategy PatternTemplate MethodFactory Pattern
NatureDesign principleBehavioral patternBehavioral patternCreational pattern
MechanismAbstraction + polymorphismComposition + delegationInheritance + hooksEncapsulated instantiation
RelationshipThe whyOne way to achieve OCPAnother way to achieve OCPSupports OCP for object creation
VariationAny axis of changeAlgorithm selection at runtimeSteps within a fixed skeletonWhich concrete type to instantiate
CouplingDefines the goalLow (composition)Medium (inheritance)Isolates new from client

Key insight for interviews: OCP is the principle. Strategy, Template Method, Decorator, and Factory are patterns that help you achieve it. When asked "how do you implement OCP?", answer with the pattern most appropriate to the context.


Advantages & Disadvantages

AdvantagesDisadvantages
New features via new code - lower regression riskIncreased number of classes/files
Easier parallel development (no merge conflicts)Indirection can reduce readability for newcomers
Promotes testability - mock interfaces easilyPremature abstraction if variation never materializes
Aligns with plugin architecturesSlight runtime overhead from virtual dispatch
Stable public APIs - consumers don't breakRequires upfront identification of variation axes
Supports Continuous Delivery - smaller, safer PRsCan conflict with YAGNI if applied too eagerly

Constraints & Edge Cases

Over-Abstraction

Creating interfaces for every class "just in case" leads to speculative generality. If there's only one implementation and no foreseeable second, an interface adds noise without value.

When Modification IS the Right Choice

  • Bug fixes - fixing incorrect behavior in existing code is modification, and that's fine.
  • Refactoring - restructuring internals without changing behavior is healthy modification.
  • Breaking changes in early-stage code - before your API has consumers, modification is cheaper than premature abstraction.
  • Cross-cutting concerns - sometimes a change genuinely affects all implementations (e.g., adding logging). Modification may be simpler than a decorator chain.

YAGNI Tension

OCP says "design for extension." YAGNI says "don't build what you don't need yet." Resolution:

  1. First occurrence - write the concrete implementation directly.
  2. Second occurrence - refactor to introduce the abstraction (Rule of Three).
  3. Known variation - if the domain guarantees variation (e.g., multiple payment providers), abstract immediately.

Sealed/Final Classes

Some designs intentionally close classes for extension (e.g., sealed in Kotlin/C#). This is a deliberate trade-off: preventing fragile base class problems at the cost of extensibility. OCP doesn't mean everything must be extensible - only the identified variation points.


Interview Follow-ups

Q1: How does OCP relate to the Dependency Inversion Principle?

Model Answer: OCP and DIP are two sides of the same coin. OCP states the goal - modules should be extendable without modification. DIP provides the mechanism - depend on abstractions, not concretions. Without DIP, high-level modules would reference concrete low-level classes directly, making extension impossible without modification. When you inject an interface rather than a concrete class, you simultaneously satisfy both principles. In practice, any OCP-compliant design will also be DIP-compliant.

Q2: Give an example where applying OCP would be harmful.

Model Answer: Consider a startup's MVP with a single payment provider (Stripe). Creating a PaymentGateway interface, a StripeGateway implementation, a factory, and a configuration layer adds 4 files and indirection for zero current benefit. If the startup pivots before ever adding a second provider, that abstraction was pure waste. The correct approach is to write the Stripe integration directly, and refactor toward OCP only when a second provider becomes a real requirement. This respects both OCP and YAGNI by applying abstraction at the point of actual variation.

Q3: How would you refactor a 500-line switch statement to respect OCP?

Model Answer: Step-by-step: (1) Identify the common behavior across all cases - this becomes your interface. (2) Extract each case into a class implementing that interface. (3) Replace the switch with a registry (map/dictionary) that maps discriminators to implementations. (4) The consumer now calls registry.get(type).execute() - polymorphic dispatch replaces branching. (5) New cases are added by registering new implementations. This is essentially the Replace Conditional with Polymorphism refactoring from Fowler's catalog. I'd do it incrementally - extract one case at a time, keeping the switch as a fallback until all cases are migrated.

Q4: Can OCP be applied to functional programming?

Hints:

  • Higher-order functions as the extension mechanism (pass behavior as arguments)
  • Function composition replaces inheritance hierarchies
  • Open maps/dictionaries of handler functions instead of switch statements
  • Clojure's multimethods and protocols as FP-native OCP

Q5: How do you handle OCP when the interface itself needs to change?

Hints:

  • Interface Segregation Principle - keep interfaces small so changes are rare
  • Versioned interfaces (PaymentGatewayV2) for backward compatibility
  • Default methods (Java 8+, C# 8+) to extend interfaces without breaking implementors
  • Adapter pattern to bridge old implementations to new interface
  • Accept that some changes are genuinely breaking - OCP minimizes but cannot eliminate modification

Counter Questions to Ask Interviewer

Use these to demonstrate depth and turn the interview into a conversation:

  1. "What's the primary axis of change in this system?" - Shows you think about where to apply OCP rather than applying it blindly everywhere.

  2. "How many concrete implementations exist today, and how often are new ones added?" - Demonstrates YAGNI awareness; OCP is most valuable when variation is frequent.

  3. "Is this a public API with external consumers, or an internal module?" - Public APIs benefit enormously from OCP (backward compatibility); internal code can tolerate modification more easily.

  4. "Are there performance constraints that make virtual dispatch problematic?" - Shows awareness of OCP's trade-offs in latency-sensitive systems (game loops, HFT).

  5. "Would you prefer composition (Strategy) or inheritance (Template Method) for this extension point?" - Demonstrates knowledge of multiple OCP implementation mechanisms.


References & Whitepapers

  1. Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall. - Original formulation of OCP through inheritance.

  2. Martin, R.C. (1996). "The Open-Closed Principle." C++ Report. - Modern reformulation using polymorphism and abstraction.

  3. Martin, R.C. (2003). Agile Software Development: Principles, Patterns, and Practices. Prentice Hall. - Chapter 9: OCP in the context of agile design.

  4. Fowler, M. (1999). Refactoring: Improving the Design of Existing Code. - "Replace Conditional with Polymorphism" refactoring.

  5. Larman, C. (2004). Applying UML and Patterns. - "Protected Variations" as an alternative framing of OCP.

  6. Gamma, E. et al. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. - Strategy, Template Method, and Decorator patterns as OCP implementations.



Last updated: 2026-05-31