Open/Closed Principle
Open for extension, closed for modification.
Open/Closed Principle (OCP): A Complete Guide with Examples in 11 Programming Languages
The Open/Closed Principle states that software entities should be open for extension but closed for modification. Once a class is written, tested, and deployed, you should be able to add new behavior to the system by adding new code — not by editing the existing, working code. OCP is the principle that keeps stable systems stable: new requirements arrive as new classes, not as surgical edits to battle-tested ones.
In this comprehensive guide, we'll explore OCP with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common violations, and clear guidance on when and how to apply this essential principle.
Table of Contents
- What is the Open/Closed Principle?
- Why Follow OCP?
- OCP Comparison: Violation vs. Compliance
- OCP Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Open/Closed Principle?
OCP is the second of the five SOLID principles, introduced by Bertrand Meyer and later refined by Robert C. Martin. Its definition is elegant: software entities (classes, modules, functions) should be open for extension but closed for modification.
Consider a payment processor that handles credit cards. A new requirement arrives: support PayPal. The naive approach opens the payment class and adds an if paymentType == "paypal" branch. Later, crypto arrives — another else if. With each new payment type, the same class is opened, edited, and re-tested. Every edit risks breaking credit card payments that were working perfectly. The class is never stable.
OCP solves this by defining an abstraction — a PaymentMethod interface — and making each payment type a separate implementation. When PayPal arrives, you add a PayPalPayment class that implements the interface. The credit card class is never touched. The processor that depends on the abstraction works automatically. The system extended without modification.
The key insight: open for extension means new behaviors can be added. Closed for modification means existing, tested code doesn't need to change to accommodate them. The mechanism that enables this is abstraction — interfaces, abstract classes, or function signatures that define a contract stable classes can depend on.
Why Follow OCP?
OCP provides several critical benefits:
- Stability: Working, tested code is never touched. Existing tests keep passing. Bug risk from new features is isolated to new code.
- Scalability: Adding the 10th payment type is as safe as adding the 2nd — no existing code changes, no regression risk on prior types.
- Parallel Development: Two developers can add two new payment types simultaneously without any file conflicts.
- Testability: Each new extension is independently testable without setting up the full system.
- Predictability: If a class is closed for modification, its behavior after testing is guaranteed — new requirements cannot silently alter it.
- Reduced Review Burden: Code reviews for new extensions only cover new code — not "did this edit to a central class break anything else?"
OCP Comparison: Violation vs. Compliance
| Dimension | OCP Violation | OCP Compliant |
|---|---|---|
| Adding new behavior | Edit existing class — add if/else | Add new class implementing an abstraction |
| Risk of regression | High — every edit touches working code | Minimal — existing classes are untouched |
| Test impact | Must re-run all tests for the modified class | Only new class needs testing |
| Parallel development | Conflicts — multiple devs editing the same class | No conflicts — each dev adds their own class |
| Growth | Class grows with every new variant | Class stays the same size; new variants are new files |
| Code review | Reviews must check for regressions | Reviews only inspect the new extension |
OCP vs. Related Concepts:
- OCP vs. Strategy Pattern: Strategy is the most direct implementation of OCP. The context is closed; strategies (extensions) are open. Every new algorithm is a new strategy class.
- OCP vs. Template Method: Template Method defines the invariant skeleton (closed) and delegates variable steps to subclasses (open). Each subclass extends without modifying the template.
- OCP vs. Plugin Architecture: Plugin systems are OCP at the system level. The core is closed; plugins extend functionality without core modification.
OCP Explained
The Abstraction Is the Key
OCP works by depending on abstractions, not concretions. A class that depends on a DiscountCalculator interface is closed — it works with any present or future discount calculator. A class that depends on ChristmasDiscountCalculator directly is open — every new type of discount forces a modification.
The abstraction creates a stable boundary. Everything above the boundary (the consumer) is closed for modification. Everything below (the implementations) can grow freely without affecting what's above.
When to Define the Abstraction
The classic mistake is creating the abstraction too early — building an elaborate interface hierarchy before you have two concrete implementations. OCP does not demand that every class be immediately surrounded by interfaces. The trigger is the second variant: when the second discount type arrives, that's when the abstraction earns its existence. "Fool me once" — the first variant taught you the shape of the extension point; the second variant confirms it.
Three OCP Implementation Strategies
- Interface/Abstract Class Inheritance: Define an interface or abstract class that captures the stable contract. New variants implement the interface. The consumer depends on the abstraction.
- Composition with Strategy: The context holds a reference to a strategy object. New strategies are new classes. The context is closed; strategies extend it without modifying it.
- Configuration/Data-Driven Extension: Store behavior in configuration, data, or a registry rather than in code. Adding a new variant means adding a new entry — no class modifications needed.
Class Diagram
Here's the UML class diagram showing a discount system refactored to comply with OCP:
Beginner-Friendly Example
Let's start with the most intuitive OCP example: a discount calculation system. We'll show the violation — a single class with growing if/else chains — then refactor to an abstraction-based design where new discount types arrive as new classes.
# ── ❌ OCP VIOLATION ───────────────────────────────────────────────
# Every new discount type requires opening and editing this class.
class DiscountCalculator:
def calculate(self, price: float, discount_type: str) -> float:
if discount_type == "none":
return price
elif discount_type == "percentage_10":
return price * 0.90
elif discount_type == "percentage_20":
return price * 0.80
elif discount_type == "flat_5":
return max(0, price - 5)
# Adding "buy_one_get_one" requires opening this class → violation
else:
raise ValueError(f"Unknown discount type: {discount_type}")
# ── ✅ OCP COMPLIANT ───────────────────────────────────────────────
from abc import ABC, abstractmethod
# The stable abstraction — never changes when new discounts are added
class DiscountCalculator(ABC):
@abstractmethod
def calculate(self, price: float) -> float:
"""Return the price after applying this discount."""
@abstractmethod
def label(self) -> str:
"""Human-readable name for this discount."""
# Each discount type is a new class — the existing classes are never touched
class NoDiscount(DiscountCalculator):
def calculate(self, price: float) -> float:
return price
def label(self) -> str:
return "No discount"
class PercentageDiscount(DiscountCalculator):
def __init__(self, percent: float) -> None:
self._percent = percent
def calculate(self, price: float) -> float:
return price * (1 - self._percent / 100)
def label(self) -> str:
return f"{self._percent}% off"
class FlatDiscount(DiscountCalculator):
def __init__(self, amount: float) -> None:
self._amount = amount
def calculate(self, price: float) -> float:
return max(0.0, price - self._amount)
def label(self) -> str:
return f"${self._amount:.2f} off"
# ✅ Adding a new discount: only ADD a new class, never edit existing ones
class BuyOneGetOneDiscount(DiscountCalculator):
def calculate(self, price: float) -> float:
return price / 2 # Effectively 50% when buying two
def label(self) -> str:
return "Buy One Get One (50% per item)"
# Consumer — closed for modification, works with any current or future discount
class PriceCalculator:
def __init__(self, discount: DiscountCalculator) -> None:
self._discount = discount
def final_price(self, original_price: float) -> float:
discounted = self._discount.calculate(original_price)
print(f" {self._discount.label()}: ${original_price:.2f} → ${discounted:.2f}")
return discounted
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Discount Calculator ===\n")
original = 100.0
discounts = [
NoDiscount(),
PercentageDiscount(10),
PercentageDiscount(20),
FlatDiscount(5),
BuyOneGetOneDiscount(),
]
for discount in discounts:
calc = PriceCalculator(discount)
calc.final_price(original)
Production-Ready Example
Now let's build a notification system — a common real-world scenario where OCP delivers maximum value. A notification system must support multiple channels (email, SMS, push, Slack) and new channels are added regularly. Without OCP, each new channel opens the central dispatcher. With OCP, each channel is an independent implementation added without touching the dispatcher.
🐍 Python
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Sequence
# ── Domain Model ───────────────────────────────────────────────────
@dataclass
class Notification:
recipient: str
subject: str
body: str
priority: str = "normal" # "normal" | "urgent"
# ── Stable Abstraction ─────────────────────────────────────────────
class NotificationChannel(ABC):
"""Defines the contract every notification channel must fulfill.
This interface is stable — it never changes as new channels are added."""
@abstractmethod
def can_handle(self, notification: Notification) -> bool:
"""Return True if this channel can handle the given notification."""
@abstractmethod
def send(self, notification: Notification) -> bool:
"""Send the notification. Return True on success."""
@abstractmethod
def channel_name(self) -> str:
"""Human-readable channel identifier."""
# ── Concrete Channels — each is an independent extension ──────────
class EmailChannel(NotificationChannel):
"""Email delivery via SMTP.
Changes only when email provider or format changes."""
def __init__(self, smtp_host: str, from_address: str) -> None:
self._smtp_host = smtp_host
self._from_address = from_address
def can_handle(self, notification: Notification) -> bool:
return "@" in notification.recipient # Valid email address
def send(self, notification: Notification) -> bool:
print(f" [EMAIL via {self._smtp_host}]")
print(f" From: {self._from_address}")
print(f" To: {notification.recipient}")
print(f" Subject: {notification.subject}")
print(f" Body: {notification.body[:50]}…")
return True
def channel_name(self) -> str:
return "Email"
class SmsChannel(NotificationChannel):
"""SMS delivery via a carrier API.
Changes only when the SMS provider or phone number format changes."""
def __init__(self, api_key: str) -> None:
self._api_key = api_key
def can_handle(self, notification: Notification) -> bool:
return notification.recipient.startswith("+") # E.164 phone number
def send(self, notification: Notification) -> bool:
print(f" [SMS via carrier API]")
print(f" To: {notification.recipient}")
print(f" Body: {notification.body[:160]}") # SMS length limit
return True
def channel_name(self) -> str:
return "SMS"
class SlackChannel(NotificationChannel):
"""Slack webhook delivery.
Changes only when the Slack API or message format changes."""
def __init__(self, webhook_url: str) -> None:
self._webhook_url = webhook_url
def can_handle(self, notification: Notification) -> bool:
return notification.recipient.startswith("#") # Slack channel
def send(self, notification: Notification) -> bool:
print(f" [SLACK → {notification.recipient}]")
print(f" *{notification.subject}*")
print(f" {notification.body}")
return True
def channel_name(self) -> str:
return "Slack"
# ✅ New requirement: add push notifications — zero existing class changes
class PushNotificationChannel(NotificationChannel):
def __init__(self, app_id: str) -> None:
self._app_id = app_id
def can_handle(self, notification: Notification) -> bool:
return notification.recipient.startswith("device:")
def send(self, notification: Notification) -> bool:
device_id = notification.recipient.replace("device:", "")
print(f" [PUSH → device:{device_id}]")
print(f" Title: {notification.subject}")
print(f" Body: {notification.body[:256]}")
return True
def channel_name(self) -> str:
return "Push"
# ── Dispatcher — closed for modification ──────────────────────────
class NotificationDispatcher:
"""Routes notifications to the appropriate channel.
This class is closed — it never changes as new channels are added."""
def __init__(self, channels: Sequence[NotificationChannel]) -> None:
self._channels = channels
def dispatch(self, notification: Notification) -> bool:
for channel in self._channels:
if channel.can_handle(notification):
print(f"\n Dispatching via {channel.channel_name()}:")
success = channel.send(notification)
if success:
print(f" ✅ Sent via {channel.channel_name()}")
else:
print(f" ❌ Failed via {channel.channel_name()}")
return success
print(f" ❌ No channel found for recipient: {notification.recipient}")
return False
def dispatch_all(self, notification: Notification) -> dict[str, bool]:
"""Send through every capable channel (broadcast mode)."""
results: dict[str, bool] = {}
for channel in self._channels:
if channel.can_handle(notification):
print(f"\n Broadcasting via {channel.channel_name()}:")
results[channel.channel_name()] = channel.send(notification)
return results
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
dispatcher = NotificationDispatcher(channels=[
EmailChannel(smtp_host="smtp.company.com", from_address="noreply@company.com"),
SmsChannel(api_key="sms-api-key-123"),
SlackChannel(webhook_url="https://hooks.slack.com/services/XXX"),
PushNotificationChannel(app_id="com.company.app"),
])
print("=== Notification Dispatch ===\n")
notifications = [
Notification("alice@company.com", "Q2 Report Ready", "Your Q2 report is now available."),
Notification("+14155552671", "Login Alert", "New login from IP 1.2.3.4 at 14:32."),
Notification("#engineering", "Deployment Complete", "v2.4.1 deployed to production ✅"),
Notification("device:iPhone-ABC123", "New Message", "You have 3 unread messages."),
]
for notif in notifications:
dispatcher.dispatch(notif)
Real-World Use Cases
OCP is most valuable wherever new variants of a behavior arrive regularly:
Payment Gateways: A PaymentGateway interface with charge() and refund() methods. StripeGateway, PayPalGateway, CryptoGateway implement it. The checkout service depends on the interface — it's closed. Adding a new gateway means adding a new class, nothing more.
Export/Serialization: A Serializer interface with serialize(data) and contentType(). JsonSerializer, XmlSerializer, CsvSerializer, ProtobufSerializer implement it. The export endpoint depends on the interface. Adding PDF export is a new PdfSerializer class.
Authentication Strategies: A AuthStrategy interface with authenticate(credentials). PasswordAuth, OAuthAuth, SamlAuth, ApiKeyAuth implement it. The auth middleware depends on the interface. Adding biometric login is a new class.
Logging/Monitoring: A LogAppender interface. FileAppender, ConsoleAppender, ElasticsearchAppender, DatadogAppender implement it. The logger depends on the interface — it's closed. New monitoring tools are new appenders.
Data Validation Rules: A ValidationRule<T> interface with validate(value). RequiredRule, EmailFormatRule, MaxLengthRule, UniqueEmailRule implement it. The validator executes a list of rules — it's closed. New business validation rules are new rule classes.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Using isinstance() Checks Instead of Polymorphism
# BAD: isinstance() is a code smell — it's a hidden OCP violation
class ReportExporter:
def export(self, report, format_type):
if isinstance(format_type, PdfFormat):
return self._export_pdf(report)
elif isinstance(format_type, CsvFormat):
return self._export_csv(report)
elif isinstance(format_type, HtmlFormat):
return self._export_html(report)
# Every new format = edit this method
# GOOD: Polymorphic dispatch — no isinstance needed
class ReportExporter:
def export(self, report, formatter: ReportFormatter) -> str:
return formatter.format(report) # Each formatter knows how to format itself
❌ Mistake #2: Growing if/elif Chains in Processing Functions
# BAD: Adding a new image filter means opening this function
def apply_filter(image, filter_name: str):
if filter_name == "grayscale":
return image.convert("L")
elif filter_name == "blur":
return image.filter(ImageFilter.BLUR)
elif filter_name == "sharpen":
return image.filter(ImageFilter.SHARPEN)
# New filter = new elif
# GOOD: Each filter is a callable; add new filters without touching existing code
class GrayscaleFilter:
def apply(self, image): return image.convert("L")
class BlurFilter:
def apply(self, image): return image.filter(ImageFilter.BLUR)
def apply_filter(image, image_filter):
return image_filter.apply(image) # Closed — no conditionals
Frequently Asked Questions
Q1: Does OCP mean I should create an interface for every class?
No. OCP does not demand interfaces everywhere — it demands that extension points be defined where variability exists or is expected. A UserRepository that saves to a database doesn't need an abstraction if you never intend to swap the database. The trigger for OCP is the second variant: when you have two implementations of the same concept, define an abstraction. Don't build the abstraction for one implementation hoping you'll need it someday.
Q2: Isn't OCP just the Strategy Pattern?
Strategy is the most direct implementation of OCP, but OCP is the broader principle. Strategy captures OCP in a specific structural form: the context is closed; strategies extend it. OCP also manifests through Template Method (the template is closed; subclasses extend steps), Decorator (the core is closed; decorators add behavior), and Plugin architecture (the system is closed; plugins extend it). OCP is the goal; Strategy (and others) is one way to achieve it.
Q3: What if the abstraction turns out to be wrong?
This is the real challenge of OCP: designing the right abstraction. A wrong abstraction — one that fits the first two cases but not the third — forces more modification than it prevents. The solution is not to abstract too early. Wait for the second variant to appear: it reveals the true shape of the extension point. Don't guess the abstraction; let real requirements teach it to you. When the abstraction proves wrong, refactor it — the cost of one refactor to fix a bad abstraction is lower than the cost of perpetual modifications to a closed system.
Q4: Is it ever OK to modify an existing class?
Yes. OCP says "closed for modification" in the context of adding new behavior variants. Fixing bugs in an existing class is not a violation — bugs are defects, not extensions. Improving performance of an existing implementation is not a violation. The "closed" part applies specifically to adding new variants of behavior: a new discount type, a new payment provider, a new export format. Those should arrive as new classes, not as edits to existing ones.
Q5: How do I apply OCP to free functions (not classes)?
In functional programming, OCP applies at the function level through higher-order functions and function composition. A function that accepts another function as a parameter is open for extension: you extend it by passing a new function. A function that checks if type == "A" is closed to extension — adding type "B" requires editing it. Higher-order functions are the functional equivalent of the Strategy pattern: the higher-order function is closed; the functions passed to it are extensions.
Q6: Can OCP lead to too many classes?
Yes — premature abstraction is a real risk. Creating an interface and ten implementations for something that only ever has one variant is overhead without value. The discipline is to apply OCP reactively: the first time you see a second variant, create the abstraction. Not before. "Speculative generality" — building extension points for variants that never arrive — creates complexity without benefit.
Q7: How does OCP relate to the "no switch statements" rule?
The rule isn't "no switch statements ever." It's "no switch statements that must grow as new variants are added." A switch on a fixed set of constants (days of the week, HTTP methods) that will never change is fine. A switch on a type that keeps growing (discount types, payment providers, notification channels) is an OCP violation waiting to happen. The test: does adding a new variant require opening this switch? If yes, the switch is violating OCP and should be replaced with polymorphism.
Q8: How do I refactor a class with a large if/else chain to comply with OCP?
- Identify the varying behavior — each branch of the if/else represents one variant.
- Define the abstraction — extract an interface with the common method signature shared by all branches.
- Extract each branch into a class — create one class per branch, each implementing the interface.
- Remove the conditional from the consumer — the consumer now holds a reference to the interface and calls it directly.
- Wire at the composition root — decide which implementation to inject at startup or based on configuration.
Key Takeaways
🎯 Core Concept: The Open/Closed Principle states that software entities should be open for extension but closed for modification. New behaviors arrive as new code (new classes, new implementations), not as edits to existing, working code. The mechanism is abstraction: a stable interface that existing code depends on, and new implementations that extend the behavior by implementing that interface.
🔑 Key Benefits:
- Regression safety: Existing, tested classes are never opened — their behavior after testing is permanent
- Parallel development: Multiple developers can add multiple extensions simultaneously without file conflicts
- Predictable growth: The 10th extension is as safe as the 2nd — no existing code ever changes
- Isolated testing: Each extension is independently testable without touching the integration
🌐 Language-Specific Highlights:
- Python: Replace
isinstance()checks andif/elifchains on type strings with ABC-based abstractions; use@abstractmethodto enforce the contract on all implementations - TypeScript: Replace discriminated union
switchstatements with interface-based dispatch; the compiler enforces the contract on every implementor - Java: Replace
enum + switchtax/payment/export logic withinterface-based strategies; userecordfor immutable value-carrying implementations - JavaScript: Use class-based implementation of a duck-typed protocol; register new handlers in a list rather than adding them to a switch
- C#: Inject
interfacedependencies via the constructor; useIOptions<T>or factory patterns to select implementations at the composition root - PHP: Use constructor promotion with
readonlyfor implementation-specific config; avoid static factory methods that switch on strings - Go: Define interfaces at the consumer side, not the provider side; embed interfaces in structs for zero-cost composition and extension
- Rust: Use
Box<dyn Trait>for runtime polymorphism; use generics with trait bounds for compile-time polymorphism — both are OCP-compliant - Dart: Use
abstract interface classfor the stable contract; useconstconstructors on stateless implementations for memory efficiency - Swift: Use
protocolfor the abstraction; store asany Protocolfor heterogeneous collections; avoidenumwith growingswitchfor behavior dispatch - Kotlin: Use
interfacefor extensions; useobjectdeclarations for stateless singleton implementations; usesealed interfaceonly when the set is intentionally closed and exhaustive
📋 When to Apply OCP:
- A class has a growing if/else or switch statement where each branch represents a different variant of the same behavior
- You are adding the same type of behavior for the second time (second payment provider, second export format, second notification channel)
- Multiple team members are competing to edit the same class for different features
- A change to one variant breaks the tests for another variant
- You need to test a behavior variant in isolation without the overhead of the full class
⚠️ When NOT to Over-Apply OCP:
- There is only one implementation and no realistic prospect of a second — abstraction adds indirection without value
- The "variation" is actually a configuration difference, not a behavioral difference — use parameters instead of classes
- The abstraction would be so specific that only one class could ever implement it
- You are adding OCP to legacy stable code that never changes — the risk of refactoring outweighs the benefit
🛠️ Best Practices Across Languages:
- Wait for the second variant: Define the abstraction when the second implementation appears — the first teaches the shape; the second confirms it
- Name the abstraction after the concept, not the implementation:
DiscountCalculatornotPercentageDiscountBase;NotificationChannelnotEmailBasedChannel - Keep the abstraction minimal: The interface should declare only what consumers actually need — no methods that exist solely for one implementation
- Wire at the composition root: Let the main function, DI container, or application startup decide which implementation to inject — not the closed classes themselves
- Use the Registry Pattern for large extension points: Store a map of
type → implementationand look up at dispatch time — adding a new type means adding one map entry
💡 Common Pitfalls:
- The "closed too early" trap: Closing a class before the true extension point is clear leads to an abstraction that fights the third variant
- Interface bloat: Adding methods to the abstraction that only one or two implementations use — this creates forced empty implementations and is an ISP violation
- Leaking concrete types through the abstraction: Returning
StripeChargefrom aPaymentGateway.charge()method couples the consumer to Stripe — abstractions must return abstract types - Closing the wrong thing: Closing the implementations and keeping the dispatcher open — it should be the opposite
- Premature abstraction: Building a six-interface hierarchy for a feature that only ever has one concrete implementation — speculation, not principle
The Open/Closed Principle is the principle that makes systems grow gracefully. In a codebase that follows OCP, adding new features is as safe as it is on day one — existing tests keep passing, existing classes are untouched, and the blast radius of every change is bounded by the walls of a single new class.
Want to dive deeper into SOLID principles? Check out our comprehensive guides on:
- Single Responsibility Principle (SRP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP) Each guide includes examples in all 11 programming languages with language-specific best practices.