🚀 SOLID Principles in JavaScript & TypeScript — Real-World Examples You Can Actually Use
“A good architecture allows major decisions to be deferred.” — Uncle Bob
Let’s face it — you’ve probably heard of SOLID principles in every other tech interview, but when it comes to JavaScript/TypeScript projects , they’re rarely applied or even understood correctly.
In this post, we’ll break down each SOLID principle with real-world examples — think e-commerce apps , notification systems , payment integrations , and more. You’ll see when to use each principle, why it matters, and how to write clean, testable, maintainable code in JS/TS.
🧱 Quick Refresher: What Is SOLID?
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
Now let’s dive into each one using examples that matter 👇
1️⃣ Single Responsibility Principle (SRP)
“A class should have only one reason to change.”
💥 Bad Example (God Class)
class OrderService {
createOrder() { /* logic */ }
calculateDiscount() { /* logic */ }
sendInvoiceEmail() { /* logic */ }
}
This class does too much — it’s handling order creation, pricing, and communication.
✅ Better SRP Example
class OrderCreator {
create(orderData) { /* persist to DB */ }
}
class DiscountCalculator {
apply(order) { /* promo code logic */ }
}
class EmailService {
sendInvoice(order) { /* SMTP/SendGrid etc. */ }
}
Now each class does one thing, and one thing well. Easier to test, refactor, and reuse.
2️⃣ Open/Closed Principle (OCP)
“Software should be open for extension, but closed for modification.”
⚠️ Problem:
You need to support different types of shipping methods — flat rate, express, pickup.
❌ Bad Design (if-else hell)
function calculateShipping(type, order) {
if (type === 'flat') return 5;
if (type === 'express') return 10;
if (type === 'pickup') return 0;
}
Every new shipping method means editing this function. Not scalable.
✅ Better OCP Design
interface ShippingStrategy {
calculate(order): number;
}
class FlatRateShipping implements ShippingStrategy {
calculate(order) { return 5; }
}
class ExpressShipping implements ShippingStrategy {
calculate(order) { return 10; }
}
class PickupShipping implements ShippingStrategy {
calculate(order) { return 0; }
}
class ShippingContext {
constructor(private strategy: ShippingStrategy) {}
getShippingCost(order) {
return this.strategy.calculate(order);
}
}
To add a new method (e.g., drone delivery), just create a new class — no core logic touched ✅
3️⃣ Liskov Substitution Principle (LSP)
“If S is a subtype of T, then T can be replaced with S without breaking the program.”
⚠️ Use Case: Notification system
You have multiple notifiers — email, SMS, push.
❌ Anti-pattern
class Notifier {
send(message: string) {}
}
class EmailNotifier extends Notifier {
send(message) { /* works */ }
}
class SMSNotifier extends Notifier {
send(message) { throw "Not implemented" }
}
You can’t substitute Notifier with SMSNotifier safely — violates LSP.
✅ LSP-Friendly Design
interface Notifier {
send(message: string): void;
}
class EmailNotifier implements Notifier {
send(msg) { console.log(`Email: ${msg}`); }
}
class SMSNotifier implements Notifier {
send(msg) { console.log(`SMS: ${msg}`); }
}
Any class implementing Notifier can now be safely substituted.
4️⃣ Interface Segregation Principle (ISP)
“Clients should not be forced to depend on interfaces they do not use.”
🤯 Real Scenario:
A User interface handles registration, login, analytics, and notifications.
❌ Overloaded Interface
interface IUser {
register(): void;
login(): void;
trackUsage(): void;
sendNotification(): void;
}
Not every User implementation needs all four.
✅ Segregated Interfaces
interface AuthUser {
register(): void;
login(): void;
}
interface Trackable {
trackUsage(): void;
}
interface Notifiable {
sendNotification(): void;
}
class BasicUser implements AuthUser {
register() {}
login() {}
}
Now each implementation depends only on what it needs.
5️⃣ Dependency Inversion Principle (DIP)
“Depend on abstractions, not on concrete implementations.”
⚠️ Scenario: A controller uses a direct DB class.
class MongoDB {
save(data) { /* ... */ }
}
class ProductController {
private db = new MongoDB(); // tightly coupled
}
What if you switch to PostgreSQL or Firebase? Everything breaks.
✅ DIP-Based Design
interface Database {
save(data): void;
}
class MongoDB implements Database {
save(data) { /* ... */ }
}
class ProductController {
constructor(private db: Database) {}
create(data) {
this.db.save(data);
}
}
Now ProductController is agnostic to the database. You can swap MongoDB with anything.
✅ TL;DR
🎯 Final Thoughts
If you’ve ever struggled with spaghetti code, adding features without breaking things, or testing tightly coupled logic — SOLID is your friend.
It’s not just for “big enterprise apps.” These patterns scale beautifully in Node.js , TypeScript , React , or even simple Express backends.
Write code that your future self (or teammate) will thank you for.
https://gist.github.com/sachinkasana/4c8e7758b13b757196dc1e67363de79d
👋 Enjoyed this post?
I write about practical software design, performance, clean code, and real-world developer patterns. You can find more of my work here:
🌐 https://sachinkasana-dev.vercel.app
Thank you for being a part of the community
Before you go:
- Be sure to clap and follow the writer ️👏 ️️
- Follow us: X | LinkedIn | YouTube | Newsletter | Podcast | Differ | Twitch
- Start your own free AI-powered blog on Differ 🚀
- Join our content creators community on Discord 🧑🏻💻
- For more content, visit plainenglish.io + stackademic.com
Top comments (0)