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

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

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

// 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

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 Reply