SOLID Principles

Five plain-language guidelines for keeping object-oriented code flexible and easy to change, bundled together under one acronym.

What is it?

Once you've internalized separation of concerns and low coupling/high cohesion, a natural question is: what does that actually look like in day-to-day object-oriented code? In the late 1990s and 2000s, engineers noticed the same five kinds of design problems showing up again and again, and gave each one a name and a guideline for avoiding it. Bundled together, they're remembered by the acronym SOLID.

- Single Responsibility Principle (SRP) — a class or module should have only one reason to change. If a ReportGenerator class both calculates report data and formats it as PDF, it has two reasons to change: a math bug and a formatting request. Split it.

- Open/Closed Principle (OCP) — code should be open for extension but closed for modification. You should be able to add new behavior (say, a new payment method) without editing the existing, already- tested code for the old ones — typically by adding a new implementation of a shared interface instead of adding another if/else branch to an existing function.

- Liskov Substitution Principle (LSP) — if code works with a base type (like Bird), it should keep working correctly if you swap in any subtype (like Penguin), without surprising breakage. A classic violation: a Penguin extends Bird where Bird has a fly() method that Penguin can't honestly implement.

- Interface Segregation Principle (ISP) — don't force a class to implement methods it doesn't need just because they're bundled into one big interface. Many small, focused interfaces beat one giant one that every implementer has to partially fake.

- Dependency Inversion Principle (DIP) — high-level logic should depend on an abstraction, not on a concrete low-level detail (this one is important enough to get its own dedicated topic next).

None of these are laws of physics — they're guidelines distilled from repeatedly seeing what makes object-oriented code brittle, and each one is really just a specific, concrete application of "low coupling, high cohesion" you already know.

Explain like I'm 10

Think of SOLID like five separate pieces of advice from a woodworking mentor: 'each tool has one job,' 'add new attachments instead of modifying the tool,' 'a replacement blade should fit the same slot,' 'don't bundle unrelated tools into one handle,' and 'design your workbench around the kind of tool, not one specific brand.'

Examples

SRP: one class, one reason to change

// Violates SRP: two reasons to change (data logic + formatting)
class Invoice {
  calculateTotal() { /* math */ }
  printAsPdf() { /* formatting */ }
}

// Follows SRP: split by responsibility
class Invoice {
  calculateTotal() { /* math */ }
}
class InvoicePdfPrinter {
  print(invoice) { /* formatting */ }
}

A bug in PDF layout no longer risks touching invoice math, and vice versa — each class has exactly one reason to change.

OCP: adding behavior without editing existing code

// Violates OCP: every new payment method edits this function
function pay(method, amount) {
  if (method === "card") return payWithCard(amount);
  if (method === "paypal") return payWithPaypal(amount);
  // adding "crypto" means editing this function again
}

// Follows OCP: new methods extend, don't modify
const paymentMethods = {
  card: payWithCard,
  paypal: payWithPaypal,
};
function pay(method, amount) {
  return paymentMethods[method](amount);
}
// Adding crypto: paymentMethods.crypto = payWithCrypto; — no edits above

The pay function's own code never needs to change again to support a new method — new behavior is added alongside it, not by editing it.

How it works

SOLID works less like a checklist to run through mechanically, and more like five lenses to view a design through. Faced with a class or module, ask: Does it have one reason to change (SRP)? Could I add a new case without editing it (OCP)? Would any of its subtypes break code written against the base type (LSP)? Is any implementer forced to support methods it doesn't need (ISP)? Does it depend on a concrete detail it could instead depend on an abstraction for (DIP)? Most real-world design smells map cleanly onto one of these five questions.

Why does it exist?

These principles were named and collected because, without them, object-oriented codebases tend to develop the same recurring diseases: god classes that do everything (violates SRP), functions with ever-growing if/else chains that break something every time a new case is added (violates OCP), subclasses that quietly break assumptions the rest of the code relies on (violates LSP), bloated interfaces nobody fully implements (violates ISP), and business logic hard-wired to one specific database or library (violates DIP). Giving each disease a name made them easier to spot and discuss.

When to use it

Reach for these as a design review lens on any object-oriented code that's expected to grow — especially before adding "just one more" branch to a function that already has several, or before subclassing something. They're most valuable exactly at the moment you're deciding how to extend existing code.

When not to use it

Applying all five principles rigidly to every tiny class, including ones that will never grow or vary, produces needless abstraction — interfaces and extension points for cases that will never arise. SOLID is a response to anticipated change; where there's truly no anticipated change, simple, direct code beats principled over-engineering.

Common mistakes

  • Treating SOLID as five independent rules to satisfy in isolation, rather than five facets of the same underlying goal: code that's easy to change safely.

  • Applying Open/Closed so aggressively that every function is wrapped in an interface 'just in case,' even where no second implementation will ever exist.

  • Confusing Liskov Substitution with 'subclasses must override every method' — LSP is about behavioral compatibility, not just having matching method signatures.

Practice exercises

  1. Easy:

    Take a class that both validates user input and saves it to a database, and split it to satisfy the Single Responsibility Principle.

  2. Medium:

    Refactor an if/else chain that picks a shipping-cost calculation based on a 'carrier' string into a design that follows the Open/Closed Principle.

  3. Hard:

    Given a Rectangle class with setWidth/setHeight, and a Square class that extends it by keeping width and height equal, explain why this violates the Liskov Substitution Principle, and propose an alternative design.

Interview questions

What does the 'S' in SOLID stand for, and what does it mean?

Single Responsibility Principle — a class or module should have only one reason to change, meaning it should have one clearly-defined job.

What's the difference between the Open/Closed Principle and just writing flexible code?

OCP specifically means you can add new behavior by adding new code (e.g., a new class implementing a shared interface) without modifying and re-testing existing, already-working code.

Can you give an example of a Liskov Substitution Principle violation?

A Square class extending a Rectangle class that has independent setWidth/setHeight methods — because setting a Square's width must also change its height to stay square, code written to work with any Rectangle can behave unexpectedly when given a Square.