December 7, 2025 (1mo ago)

A Developer's Guide to the Open Closed Principle

Master the Open Closed Principle with practical examples in TypeScript and React. Learn to write scalable, maintainable code that adapts to change.

← Back to blog
Cover Image for A Developer's Guide to the Open Closed Principle

Master the Open Closed Principle with practical examples in TypeScript and React. Learn to write scalable, maintainable code that adapts to change.

Title: A Developer's Guide to the Open Closed Principle

Summary: Master the Open-Closed Principle with practical examples in TypeScript and React. Learn to write scalable, maintainable code that adapts to change.

Introduction: Master the Open-Closed Principle with practical examples in TypeScript and React. Learn to write scalable, maintainable code that adapts to change.

Tags: open closed principle, solid principles, clean code, typescript, software architecture

The Open-Closed Principle (OCP) is one of those concepts that sounds a bit academic at first, but it’s incredibly practical. It boils down to this: software components should be open for extension, but closed for modification.

In simple terms, it means you should be able to add new features to your application without having to dig into existing, proven code and change it. It’s a powerful strategy for building software that can grow and adapt without constantly breaking.

The foundation of adaptable code

An architectural drawing depicting a house interior with various networked devices and labeled connections.

Think of your software like a brand-new house. The electrical wiring is all set up, it’s been tested, and it's sealed behind the drywall. Now you decide to add a smart TV, a gaming console, and a new sound system.

You don't start tearing down the walls to rewire everything. That would be chaotic and risky. Instead, you simply plug your new gadgets into the existing outlets.

That's OCP in a nutshell. The house's electrical system is “closed for modification”—its core wiring is stable and shouldn’t be touched. But it’s also “open for extension” because the outlets provide a standard way to add new appliances without disrupting the whole system.

What does “open for extension” mean?

Design modules, classes, or functions so their behaviour can be augmented without changing their internals. Introduce new capabilities by adding new code, not by editing old, tested code.

What does “closed for modification” mean?

Protect existing, stable code. Once a component has been developed, tested, and deployed, avoid altering its source just to add new features. Every change to existing code risks introducing regressions.

The Open-Closed Principle isn't just theory; it’s a cornerstone of modern software engineering. Teams that apply OCP reduce maintenance churn and keep core systems stable while adding features rapidly.1

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

This guideline is the second pillar of SOLID and it works closely with the other principles to foster strong architecture. For example, you can’t really follow OCP without focused, single-purpose components—the point of the Single Responsibility Principle. See our guide on the Single Responsibility Principle.

When your team embraces this principle, you sidestep fragile, tightly coupled systems that break with every new requirement. The result is a codebase that’s more resilient, maintainable, and predictable for both human developers and AI pair-programmers. Industry research highlights the growing impact of design principles on engineering practices and productivity.2

How to implement OCP with TypeScript

Move from theory to real-world code. In TypeScript, abstraction and polymorphism are the main tools. Use interfaces and abstract classes so components depend on stable contracts instead of brittle implementations.

When one part of your system depends on an abstraction (like an interface), you can add features by creating new classes that follow that contract. The original code doesn’t change because it only cares about the contract.

A hand-drawn software diagram illustrates Interface, Concrete, and Indirect classes with dependencies.

A common OCP violation

A typical anti-pattern is a switch statement that dispatches behaviour by type. For example, a PaymentProcessor that switches on paymentMethod must be modified for every new method.

// BEFORE: Violates the Open/Closed Principle
type PaymentMethod = 'credit_card' | 'paypal' | 'crypto';

interface Order {
  id: string;
  amount: number;
  paymentMethod: PaymentMethod;
}

class PaymentProcessor {
  process(order: Order): void {
    switch (order.paymentMethod) {
      case 'credit_card':
        console.log(`Processing credit card payment for order ${order.id}...`);
        break;
      case 'paypal':
        console.log(`Processing PayPal payment for order ${order.id}...`);
        break;
      case 'crypto':
        console.log(`Processing crypto payment for order ${order.id}...`);
        break;
      default:
        throw new Error('Unsupported payment method');
    }
  }
}

Every time the business adds Apple Pay, this class must change. It’s not closed for modification.

Refactoring to adhere to OCP

Introduce an abstraction. Define a PaymentStrategy interface with a common pay method and implement a class per payment method.

Depending on an abstract interface instead of a concrete class decouples components and makes testing easier by allowing mocks.

// AFTER: Adheres to the Open/Closed Principle
interface PaymentStrategy {
  pay(order: Order): void;
}

class CreditCardPayment implements PaymentStrategy {
  pay(order: Order): void {
    console.log(`Processing credit card payment for order ${order.id}...`);
  }
}

class PayPalPayment implements PaymentStrategy {
  pay(order: Order): void {
    console.log(`Processing PayPal payment for order ${order.id}...`);
  }
}

class CryptoPayment implements PaymentStrategy {
  pay(order: Order): void {
    console.log(`Processing crypto payment for order ${order.id}...`);
  }
}

class OcpPaymentProcessor {
  process(order: Order, paymentStrategy: PaymentStrategy): void {
    paymentStrategy.pay(order);
  }
}

To add Apple Pay, create ApplePayPayment implementing PaymentStrategy. No existing code changes.

This pattern keeps systems flexible, testable, and maintainable. It’s the approach we use across projects like microestimates.com.

Applying OCP in React and Node.js

OCP shines across frontend and backend. Below are concrete patterns for React components and Node.js services.

Hand-drawn concept showing a UI design evolving from a static layout to a dynamic, modular one.

A flexible Card component in React

A common anti-pattern is a Card component with a type prop and conditional rendering. That component becomes a magnet for future changes.

// BEFORE: Violates the Open-Closed Principle
interface CardProps {
  type: 'user' | 'product' | 'news';
  data: any;
}

const Card = ({ type, data }: CardProps) => {
  if (type === 'user') {
    return <div><h2>{data.name}</h2><p>{data.email}</p></div>;
  } else if (type === 'product') {
    return <div><h3>{data.title}</h3><span>${data.price}</span></div>;
  } else if (type === 'news') {
    return <div><h1>{data.headline}</h1><p>{data.summary}</p></div>;
  }
  return null;
};

A better approach uses composition. Make Card a stable wrapper, then create specialized components that use it.

// AFTER: Adheres to the Open/Closed Principle
interface CardProps {
  children: React.ReactNode;
  className?: string;
}

const Card = ({ children, className }: CardProps) => (
  <div className={`card ${className}`}>{children}</div>
);

const UserProfileCard = ({ user }) => (
  <Card className="user-profile">
    <h2>{user.name}</h2>
    <p>{user.email}</p>
  </Card>
);

const ProductSummaryCard = ({ product }) => (
  <Card className="product-summary">
    <h3>{product.title}</h3>
    <span>${product.price}</span>
  </Card>
);

Now the base Card is closed for modification, while the system stays open to new card types.

An extensible payment service in Node.js

A Node.js PaymentService that switches on provider is fragile. Refactor using the Strategy pattern.

// BEFORE: Violates the Open-Closed Principle
class PaymentService {
  processPayment(provider: 'stripe' | 'paypal', amount: number) {
    switch (provider) {
      case 'stripe':
        console.log(`Processing $${amount} via Stripe.`);
        break;
      case 'paypal':
        console.log(`Processing $${amount} via PayPal.`);
        break;
      default:
        throw new Error('Unsupported payment provider');
    }
  }
}

Refactor to use a PaymentProvider interface and concrete classes.

// AFTER: Adheres to the Open-Closed Principle
interface PaymentProvider {
  process(amount: number): void;
}

class StripeProvider implements PaymentProvider {
  process(amount: number) {
    console.log(`Processing $${amount} via Stripe.`);
  }
}

class PayPalProvider implements PaymentProvider {
  process(amount: number) {
    console.log(`Processing $${amount} via PayPal.`);
  }
}

class PaymentService {
  processPayment(provider: PaymentProvider, amount: number) {
    provider.process(amount);
  }
}

Adding Adyen or Braintree means adding a new provider class. The PaymentService remains untouched.

OCP violation vs. adherence patterns

ScenarioCode Violating OCP (Before)Code Adhering to OCP (After)
Rendering UI variantsSingle component with a large switch on typeGeneric container accepts specialized components as children
Data export logicexportData(format, data) with if/else blocksExporter interface with CsvExporter, JsonExporter classes
Discount calculationcalculateDiscount with many if chainsDiscountStrategy interface with concrete strategies
Handling user actionsMassive reducer with one switch on action.typeSmaller reducers, composition, middleware
Backend routingOne routing file with dozens of routesModular routing with per-resource routers

The “Before” column contains code that constantly needs changing; the “After” column shows patterns that favor adding isolated modules. That shift is the heart of OCP.

Common OCP mistakes and how to avoid them

Knowing OCP is one thing; applying it correctly is another. These are the traps teams fall into and how to avoid them.

Hand-drawn diagrams illustrating OCP mistakes and a refactored strategy pattern, with various boxes and arrows.

The endless if/else or switch chain

This classic violation centralizes logic and forces modification whenever a new type is added. Move behavior into polymorphic classes that implement a shared interface.

// ANTI-PATTERN
function getAnimalSound(animal: { type: string }): string {
  if (animal.type === 'dog') return 'Woof';
  if (animal.type === 'cat') return 'Meow';
  if (animal.type === 'cow') return 'Moo';
  return '';
}

// REFACTORED
interface Animal { makeSound(): string }
class Dog implements Animal { makeSound() { return 'Woof' } }
class Cat implements Animal { makeSound() { return 'Meow' } }

To add a sheep, create a Sheep class. No existing code changes.

The fragile base class problem

When a base class provides too much concrete behavior, changes to it break subclasses. Prefer composition over inheritance and keep base classes minimal—define contracts, not heavy implementations.

The pitfall of premature generalization

Over-abstraction “just in case” leads to a hard-to-understand, over-engineered codebase. Apply OCP where change is likely: business rules, export formats, third-party integrations. When code is modified repeatedly for the same reason, that’s the signal to refactor toward OCP.

Modular design and good interface boundaries improve maintainability and can reduce defect resolution time in large public-sector modernization projects, where modular practices are recommended by state digital service guidelines.3

By avoiding these mistakes, you’ll build systems that are ready for future growth without unnecessary complexity.

Why OCP matters for your business and team

OCP isn’t academic. It’s a strategic approach that turns code from a liability into a flexible asset. When your software is open for extension but closed for modification, you reduce the “complexity tax” that slows feature delivery.

Driving down maintenance costs

Maintenance can dominate lifecycle costs. Teams that reduce the need to modify core systems lower development time, shrink QA cycles, and reduce expensive bug fixes. Apply OCP to protect stable code and cut long-term ownership costs.1

Accelerating your team’s velocity

OCP requires upfront thought, but it pays off. With a stable foundation, adding features becomes faster and more predictable. Your team builds on a platform, not inside a fragile core.

Mitigating risk

Every change carries risk. Adding a new, self-contained class is safer than editing battle-tested code. OCP creates clear boundaries that shield critical logic from accidental side effects. That stability is vital for startups and large systems alike.

Weaving OCP into your team’s workflow

Adoption is a team sport. Embed OCP into code reviews, training, and tooling.

Make OCP part of code reviews

Use PR checklists with questions like:

  • “If we needed a new [type/strategy/option], would this file need to change?”
  • “Is this switch or long if/else likely to grow?”
  • “Could this logic be pulled into a strategy or plugin?”

Focus on parts of the codebase that change the most: business rules, integrations, and UI libraries.

Tools and training

Linters and static analysis can flag OCP anti-patterns. Focused training and hands-on workshops help teams internalize these design habits. For more on how these patterns fit with testing, see our guide on the Red-Green-Refactor TDD cycle.

Common questions about OCP

“Does ‘closed for modification’ mean I can never touch a file again?”

No. It’s a guiding principle for adding features. Bug fixes and necessary maintenance are different. OCP is about avoiding edits to stable code when introducing new behavior.

“Isn’t this just over-engineering?”

It can be if applied everywhere. Be pragmatic: apply OCP where change is likely. When you modify the same code repeatedly, that’s the cue to refactor toward OCP.

“How does OCP fit with the other SOLID principles?”

They work together. SRP creates small classes that are easy to close. LSP ensures your extensions won’t break existing behavior. DIP provides the abstractions OCP depends on.


Is your team wrestling with technical debt or looking to build a codebase that scales with AI-powered development? Clean Code Guy provides codebase audits, hands-on refactoring, and workshops to master principles like OCP. Learn how we can help you ship better software, faster: https://cleancodeguy.com

Q&A — three concise user-focused questions and answers

Q: How do I spot an OCP violation in my codebase?

A: Look for long if/else or switch chains, large components that change when new variants are added, or base classes that frequently require edits. If a single file changes every time you add a feature, that’s a sign.

Q: When should I refactor for OCP?

A: Refactor when a piece of code is modified repeatedly for new behavior. Start small: create an interface or wrapper and move specific logic into new classes. Don’t over-engineer beforehand.

Q: What practical steps help a team adopt OCP?

A: Add OCP checks to code reviews, teach common patterns (strategy, composition), use linters or static rules to detect anti-patterns, and run focused workshops with real code examples.

1.
Sean Carpenter, “The Cost of Software Maintenance,” SEI Blog, Carnegie Mellon University Software Engineering Institute, June 4, 2018, https://insights.sei.cmu.edu/sei_blog/2018/06/the-cost-of-software-maintenance.html.
2.
Stanford University, “AI Index Report,” https://aiindex.stanford.edu/report/.
3.
California Department of Education, Digital Services Principles and Guidance, https://www.cde.ca.gov/ta/ac/cm/dbprinciples.asp.
← Back to blog
🙋🏻‍♂️

AI writes code.
You make it last.

In the age of AI acceleration, clean code isn’t just good practice — it’s the difference between systems that scale and codebases that collapse under their own weight.