SOLID Principles Explained: Write Code That Does Not Break When Requirements Change

SOLID Principles Explained: Write Code That Does Not Break When Requirements Change
8 views

SOLID principles are not abstract rules — they are lessons learned from decades of codebases that became impossible to change. Code that violates SOLID tends to grow tangled: a small change here breaks something there, you cannot test this without that, you add features by copy-pasting instead of extending. These five principles are the antidote.

⚡ TL;DR: S — one reason to change. O — extend without modifying. L — subtypes must be substitutable. I — small specific interfaces. D — depend on abstractions. Together: code where adding features means adding code, not changing existing code.

S — Single Responsibility Principle — SOLID principles

// VIOLATION: User class does too many things
class User {
  constructor(public name: string, public email: string) {}
  save() { /* SQL insert logic */ }          // Database concern
  sendWelcomeEmail() { /* SMTP logic */ }    // Email concern
  generateReport() { /* PDF logic */ }       // Reporting concern
}
// One change to email sending = touching User = risk to save() and report()

// CORRECT: one class per responsibility
class User { constructor(public name: string, public email: string) {} }
class UserRepository { save(user: User) { /* SQL */ } }
class EmailService { sendWelcome(user: User) { /* SMTP */ } }
class UserReportGenerator { generate(user: User) { /* PDF */ } }
// Change email logic → only touch EmailService → zero risk to others

O — Open/Closed Principle

// VIOLATION: add new payment type = modify existing code
class PaymentProcessor {
  process(type: string, amount: number) {
    if (type === 'stripe') { /* Stripe logic */ }
    else if (type === 'paypal') { /* PayPal logic */ }
    // Adding Apple Pay = modify this class = risk to existing payments
  }
}

// CORRECT: open for extension, closed for modification
interface PaymentGateway {
  charge(amount: number): Promise;
}
class StripeGateway implements PaymentGateway { async charge(amount) { /* */ } }
class PayPalGateway implements PaymentGateway { async charge(amount) { /* */ } }
class ApplePayGateway implements PaymentGateway { async charge(amount) { /* */ } }

class PaymentProcessor {
  constructor(private gateway: PaymentGateway) {}
  async process(amount: number) { return this.gateway.charge(amount); }
}
// Add Apple Pay = new class, zero changes to existing code

L — Liskov Substitution Principle

// VIOLATION: subtype breaks parent contract
class Rectangle {
  constructor(protected width: number, protected height: number) {}
  setWidth(w: number) { this.width = w; }
  setHeight(h: number) { this.height = h; }
  area() { return this.width * this.height; }
}

class Square extends Rectangle {
  setWidth(w: number) { this.width = this.height = w; } // Breaks LSP!
  setHeight(h: number) { this.width = this.height = h; }
}

function test(rect: Rectangle) {
  rect.setWidth(5); rect.setHeight(10);
  console.log(rect.area()); // Rectangle: 50, Square: 100 — broken!
}
// CORRECT: Square should not extend Rectangle if it changes the contract
// Make both implement a Shape interface instead

I — Interface Segregation

// VIOLATION: fat interface forces unnecessary methods
interface Animal {
  eat(): void;
  sleep(): void;
  fly(): void;   // Dogs can't fly but must implement!
  swim(): void;  // Eagles can't swim but must implement!
}

// CORRECT: small, specific interfaces
interface Eatable { eat(): void; }
interface Sleepable { sleep(): void; }
interface Flyable { fly(): void; }
interface Swimmable { swim(): void; }

class Dog implements Eatable, Sleepable, Swimmable {
  eat() {} sleep() {} swim() {}
  // No fly() — dogs don't need it
}
class Eagle implements Eatable, Sleepable, Flyable {
  eat() {} sleep() {} fly() {}
}

D — Dependency Inversion Principle

// VIOLATION: high-level module depends on low-level detail
class OrderService {
  private db = new MySQLDatabase(); // Direct dependency on MySQL
  // Can't test without MySQL. Can't switch to PostgreSQL without rewriting.
  async createOrder(order: Order) { await this.db.save(order); }
}

// CORRECT: depend on abstraction (interface), not implementation
interface Database { save(data: unknown): Promise; }
class MySQLDatabase implements Database { async save(data) { /* */ } }
class PostgreSQLDatabase implements Database { async save(data) { /* */ } }
class InMemoryDatabase implements Database { async save(data) { /* */ } }

class OrderService {
  constructor(private db: Database) {} // Injected — depends on abstraction
  async createOrder(order: Order) { await this.db.save(order); }
}
// Test: new OrderService(new InMemoryDatabase())
// Production: new OrderService(new MySQLDatabase())
// Switch DB: new OrderService(new PostgreSQLDatabase()) — zero code changes

SOLID quick reference

  • ✅ S: If you need “and” to describe what a class does, split it
  • ✅ O: Adding features should mean adding new code, not modifying existing code
  • ✅ L: If you need instanceof checks, your inheritance is violating LSP
  • ✅ I: Interfaces should describe one role, not everything an object can do
  • ✅ D: Pass dependencies as constructor params — makes testing trivial

SOLID principles connect directly to the Gang of Four design patterns — patterns like Strategy, Factory, and Observer are SOLID principles made concrete. External reference: SOLID — Wikipedia.

Recommended Reading

→ Designing Data-Intensive Applications — Essential for every senior developer. Distributed systems, databases, and production architecture.

→ The Pragmatic Programmer — Timeless engineering wisdom for writing better code at any level.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Don’t Miss the Next Cheat Code

Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Related: RAG for Code: Build a Codebase-Aware AI Assistant That Does Not Hallucinate Your API · Vibe Coding: How Senior Developers Actually Use AI to Write Code Faster · Python Type Hints Complete Guide: Write Self-Documenting, Bug-Free Code

Related: TypeScript Generics Deep Dive: Write Reusable Code With Full Type Safety · Day 94: Change Management for Prompts · Prompt Engineering for Developers: Patterns That Produce Production-Ready Code Every Time

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.