Ever read about SOLID and felt your eyelids droop halfway through the “S”? Let’s fix that.
Whether you're debugging spaghetti code at 2 AM or trying to prevent future you from rage-quitting, SOLID principles are here to help. But let's be honest—they’re often taught in the driest way possible.
In this article, I’ll walk you through the SOLID principles using TypeScript, real-world analogies, and some much-needed humor. No jargon. No hand-waving. Just clean, readable code examples you’ll understand and remember.
Why SOLID Matters (And Why It’s Often Boring)
SOLID is a set of five design principles that make your code:
Easier to maintain
Easier to test
Easier to extend without breaking things
Yet, most explanations feel like they were written by a robot for other robots. That ends now.
Let’s break each principle down like we're refactoring your first bootcamp project—one bug at a time.
🧱 S – Single Responsibility Principle (SRP)
"One class should have one, and only one, reason to change."
Translation: One class. One job. No multitasking.
🚨 The Violation:
class User {
constructor(public username: string, public password: string) {}
login() {
// auth logic
}
saveToDB() {
// db logic
}
logActivity() {
// logging logic
}
}
This class is doing way too much. It’s authenticating, persisting data, and logging. Next it’ll be making coffee.
✅ The Fix:
class AuthService {
login(username: string, password: string) {
// auth logic
}
}
class UserRepository {
save(user: User) {
// db logic
}
}
class Logger {
log(message: string) {
console.log(message);
}
}
class User {
constructor(public username: string, public password: string) {}
}
Each class now has one job. You just earned a future-you high five.
🚪 O – Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
Translation: Add new features by adding code, not rewriting old code.
🚨 The Violation:
class Payment {
pay(method: string) {
if (method === 'paypal') {
// PayPal logic
} else if (method === 'stripe') {
// Stripe logic
}
}
}
Every time you add a new payment method, you have to edit this class. That’s a maintenance landmine.
✅ The Fix:
interface PaymentMethod {
pay(amount: number): void;
}
class PayPal implements PaymentMethod {
pay(amount: number) {
console.log(`Paid ${amount} using PayPal`);
}
}
class Stripe implements PaymentMethod {
pay(amount: number) {
console.log(`Paid ${amount} using Stripe`);
}
}
class PaymentProcessor {
constructor(private method: PaymentMethod) {}
process(amount: number) {
this.method.pay(amount);
}
}
Now we can add a new payment method without touching existing logic. That’s OCP in action.
🦢 L – Liskov Substitution Principle (LSP)
"Subtypes must be substitutable for their base types."
Translation: If Dog
extends Animal
, it should still behave like an Animal
.
🚨 The Violation:
class Rectangle {
constructor(public width: number, public 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 = w;
this.height = w;
}
setHeight(h: number) {
this.width = h;
this.height = h;
}
}
Try using Square
where Rectangle
is expected and you'll get surprising behavior. That breaks LSP.
✅ The Fix:
Don’t extend Rectangle
for Square
. Either compose or create separate interfaces. LSP prefers behavioral compatibility over inheritance convenience.
🧩 I – Interface Segregation Principle (ISP)
"Clients shouldn’t be forced to depend on methods they don’t use."
Translation: Keep your interfaces lean and focused.
🚨 The Violation:
interface Machine {
print(): void;
scan(): void;
fax(): void;
}
class OldPrinter implements Machine {
print() {
console.log("Printing...");
}
scan() {
throw new Error("Not supported");
}
fax() {
throw new Error("Not supported");
}
}
Why should a printer implement scan and fax?
✅ The Fix:
interface Printer {
print(): void;
}
interface Scanner {
scan(): void;
}
class SimplePrinter implements Printer {
print() {
console.log("Printing...");
}
}
Now classes only implement what they actually need. The principle is happy. So is your error log.
🔌 D – Dependency Inversion Principle (DIP)
"Depend on abstractions, not concretions."
Translation: Don’t hardcode your dependencies. Use interfaces.
🚨 The Violation:
class UserService {
private db = new Database(); // tight coupling
getUser(id: string) {
return this.db.find(id);
}
}
You can’t swap Database
with MockDatabase
or CloudDatabase
. Testing? Good luck.
✅ The Fix:
interface IDatabase {
find(id: string): any;
}
class UserService {
constructor(private db: IDatabase) {}
getUser(id: string) {
return this.db.find(id);
}
}
Now you can inject any database implementation. Your code is more flexible—and testable.
🔑 Key Takeaways
SRP: One class = one job. Keep it focused.
OCP: Extend behavior without changing existing code.
LSP: Subclasses should work like their parents.
ISP: Don’t force classes to implement what they don’t need.
DIP: Code to interfaces, not implementations.
Let’s Keep This Going 🚀
Which SOLID principle trips you up the most? Drop a comment—let’s discuss!
If this helped, share it with a friend who still thinks SRP is a WiFi protocol.
🌐 Connect With Me On:
📍 LinkedIn
📍 X (Twitter)
📍 Telegram
📍 Instagram
Happy Coding!
Top comments (2)
Thank you for the engaging article and clear explanations! It was really great and definitely useful for us developers. Appreciate you taking the time to make these complex concepts easy to understand.
Thank you!