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 othersO — 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 insteadI — 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.
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.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
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.
