DT
DomTek SOLID Principles

SOLID principles, explained like you’ll actually use them.

SOLID is a set of five design principles that help you build software that’s easier to change, test, and maintain. This page gives you an intuitive definition, common smells, and a small example for each.

S ·· Single Responsibility O ·· Open/Closed L ·· Liskov Substitution I ·· Interface Segregation D ·· Dependency Inversion
Start learning

Principles

Each card includes a definition, red flags, and a compact example.

Tip: use the search bar to filter cards.

S Single Responsibility Principle
SRP

One module should have one reason to change. Keep responsibilities cohesive.

What to watch for
  • Classes that do I/O, validation, formatting, and persistence in one place.
  • Changes in one feature repeatedly break unrelated behavior.
  • Lots of if/else branches for different “modes” or “types.”
// Before: one class doing many jobs
class InvoiceService {
  generate(invoice) { /* build invoice */ }
  email(invoice)    { /* send email */ }
  save(invoice)     { /* write to DB */ }
}

// After: split responsibilities
class InvoiceGenerator { generate(i) { /* ... */ } }
class InvoiceMailer    { email(i)    { /* ... */ } }
class InvoiceRepo      { save(i)     { /* ... */ } }
O Open/Closed Principle
OCP

Open for extension, closed for modification. Add behavior without editing stable code.

What to watch for
  • Adding a new case requires editing a big switch statement.
  • Every new feature touches the same “core” file.
  • Feature flags and conditionals multiply.
// Before: switch grows forever
function price(order) {
  switch (order.type) {
    case 'standard': return order.base;
    case 'vip':      return order.base * 0.9;
    // new types require editing this function
  }
}

// After: extend via strategy
class PricingRule { price(order) {} }
class StandardRule extends PricingRule { price(o){ return o.base } }
class VipRule extends PricingRule { price(o){ return o.base * 0.9 } }

function priceWith(rule, order){ return rule.price(order) }
L Liskov Substitution Principle
LSP

Subtypes must be usable where the base type is expected··without surprises.

What to watch for
  • Overridden methods that throw “not supported” for valid base behavior.
  • Subclasses that narrow inputs or weaken guarantees.
  • Callers checking instanceof to avoid breakage.
// Classic smell: subtype breaks expectations
class Rectangle {
  setWidth(w){ this.w = w }
  setHeight(h){ this.h = h }
  area(){ return this.w * this.h }
}

class Square extends Rectangle {
  setWidth(w){ this.w = this.h = w }
  setHeight(h){ this.w = this.h = h }
}

// Caller expects independent width/height
function resizeTo(rect){
  rect.setWidth(5)
  rect.setHeight(10)
  return rect.area() // 50 for Rectangle, 100 for Square (surprise)
}
I Interface Segregation Principle
ISP

Prefer small, client-specific interfaces. Don’t force consumers to depend on methods they don’t use.

What to watch for
  • Interfaces with many methods, where most implementations return defaults.
  • Mocking pain: tests must stub unrelated methods.
  • Changing one method breaks many unrelated implementers.
// Before: one fat interface
interface Worker {
  work(): void
  eat(): void
}

class Robot implements Worker {
  work(){ /* ... */ }
  eat(){ throw new Error('N/A') }
}

// After: split interfaces
interface Workable { work(): void }
interface Eatable  { eat(): void }

class Robot implements Workable { work(){ /* ... */ } }
D Dependency Inversion Principle
DIP

Depend on abstractions, not concretions. High-level policy shouldn’t know low-level details.

What to watch for
  • Business logic instantiates databases, HTTP clients, or frameworks directly.
  • Hard to test because collaborators can’t be swapped.
  • Changes to infrastructure ripple into core modules.
// Before: high-level depends on low-level
class ReportService {
  constructor(){ this.db = new SqlDatabase() }
  run(){ return this.db.query('...') }
}

// After: depend on abstraction
class Database { query(q){} }
class SqlDatabase extends Database { query(q){ /* ... */ } }

class ReportService {
  constructor(db /* Database */){ this.db = db }
  run(){ return this.db.query('...') }
}

// Composition root wires things together:
// new ReportService(new SqlDatabase())

Practical patterns that often help

Not rules··tools. Apply when you feel friction from change or testing.

  • SRP: Extract services (validation, formatting, persistence), or use a pipeline of small steps.
  • OCP: Strategy, plugin architectures, event handlers, polymorphism, or rule registries.
  • LSP: Prefer composition over inheritance; encode contracts with tests.
  • ISP: Split by consumer use-cases; keep interfaces tiny and stable.
  • DIP: Dependency injection; keep framework code at the edges (“ports & adapters”).

Self-check checklist

Tap items to mark them done. Saved in your browser.

Goal: internalize the trade-offs, not memorize the acronym.

SRP Each module has a single reason to change.
OCP Extend behavior without editing stable code.
LSP Subtypes preserve the base contract.
ISP Small interfaces tailored to clients.
DIP Depend on abstractions; inject details.
Tests as contracts Encode expectations so changes are safe.
Frameworks at the edges Keep core logic independent from infrastructure.
Refactor with intent Improve one pain point; avoid “big rewrites.”

FAQ

Common questions teams ask when adopting SOLID.

? Do I need all five?

Treat SOLID as diagnostic lenses. When change is painful, tests are hard, or coupling is high, one (or more) principles usually points to a refactor direction.

Rule of thumb
  • Optimize for change frequency: refactor where you touch code most often.
  • Prefer simple solutions until a pattern pays for itself.
  • Measure with outcomes: fewer regressions, faster changes, clearer tests.
! Isn’t this “over-engineering”?

It can be··if you introduce abstractions without a real need. SOLID doesn’t demand abstractions; it helps you choose them when the cost of change is already hurting.

How to avoid it
  • Start with concrete code; abstract once you see repeated variation.
  • Keep interfaces small and named after behavior, not technology.
  • Refactor incrementally: extraction, tests, then substitution.