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.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
