System Design
π What are SOLID Principles?
SOLID is a set of five design principles that help us write better, cleaner, and more maintainable code. They are:
SOLID is a set of five design principles that help us write better, cleaner, and more maintainable code. They are:
- S β Single Responsibility Principle (SRP)
- O β Open/Closed Principle (OCP)
- L β Liskov Substitution Principle (LSP)
- I β Interface Segregation Principle (ISP)
- D β Dependency Inversion Principle (DIP)
Think of them as rules for good architecture β just like traffic rules help cars move smoothly, these principles help code work smoothly in big systems.
1οΈβ£ Single Responsibility Principle (SRP)
π A class or function should do only one thing (have only one reason to change).
β Bad Example (doing too many things):
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
saveUser() {
// Save user to database
}
sendEmail() {
// Send welcome email
}
}Here, User is handling data, database, and email β Too many responsibilities.
β Good Example (split responsibilities):
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
class UserRepository {
save(user) {
// Save user to database
}
}
class EmailService {
sendEmail(email, message) {
// Send email
}
}Now:
Useronly holds data.UserRepositoryhandles saving.EmailServicehandles email.
π Each has one reason to change β Clean!
2οΈβ£ Open/Closed Principle (OCP)
π Code should be open for extension but closed for modification. This means: You should add new features without touching old code.
β Bad Example:
function calculateDiscount(type, price) {
if (type === "regular") {
return price * 0.9;
} else if (type === "premium") {
return price * 0.8;
}
}Problem: If tomorrow we add "gold" customers, we need to modify this function β risk of breaking existing logic.
β Good Example:
class Discount {
calculate(price) {
return price;
}
}
class RegularDiscount extends Discount {
calculate(price) {
return price * 0.9;
}
}
class PremiumDiscount extends Discount {
calculate(price) {
return price * 0.8;
}
}Now, if we want "GoldDiscount", we just add a new class β no need to touch old code.
3οΈβ£ Liskov Substitution Principle (LSP)
π If A is a parent class, then its child B should work everywhere A works without breaking things.
β Bad Example:
class Bird {
fly() {
console.log("Flying...");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Penguins canβt fly!");
}
}Here, Penguin breaks the rule because it canβt behave like a Bird.
β Good Example:
class Bird {
move() {
console.log("Moving...");
}
}
class Sparrow extends Bird {
move() {
console.log("Flying...");
}
}
class Penguin extends Bird {
move() {
console.log("Swimming...");
}
}Now all birds can move (fly/swim) β no broken logic.
4οΈβ£ Interface Segregation Principle (ISP)
π Donβt force a class to implement things it doesnβt need.
β Bad Example:
class Machine {
print() {}
scan() {}
fax() {}
}
class Printer extends Machine {
scan() {
throw new Error("I canβt scan!");
}
fax() {
throw new Error("I canβt fax!");
}
}Here, Printer is forced to implement useless methods.
β Good Example:
class Printer {
print() {}
}
class Scanner {
scan() {}
}
class Fax {
fax() {}
}Now, a simple Printer only prints. A multifunctional device can combine all.
5οΈβ£ Dependency Inversion Principle (DIP)
π High-level modules (business logic) should not depend on low-level details. Both should depend on abstractions.
β Bad Example:
class MySQLDatabase {
save(data) {
console.log("Saving to MySQL");
}
}
class UserRepository {
constructor() {
this.db = new MySQLDatabase(); // tightly coupled
}
saveUser(user) {
this.db.save(user);
}
}Problem: If tomorrow we want MongoDB, we need to change UserRepository.
β Good Example:
class Database {
save(data) {}
}
class MySQLDatabase extends Database {
save(data) {
console.log("Saving to MySQL");
}
}
class MongoDBDatabase extends Database {
save(data) {
console.log("Saving to MongoDB");
}
}
class UserRepository {
constructor(database) {
this.db = database; // depends on abstraction
}
saveUser(user) {
this.db.save(user);
}
}Now, UserRepository can work with any database β just pass the one you need.
π― Summary in Simple Words
- S (Single Responsibility): One job per class/function.
- O (Open/Closed): Add new stuff without changing old stuff.
- L (Liskov): Subclasses should not break parent behavior.
- I (Interface Segregation): Donβt force things to implement what they donβt need.
- D (Dependency Inversion): Depend on abstractions, not concrete details.
π Following SOLID makes your code:
- Easier to understand
- Easier to test
- Easier to extend in future
Mini E-commerce Checkout System (with SOLID)
1. S β Single Responsibility Principle (SRP)
π Split responsibilities: User data, Cart, Payment, and Notification.
// β
User just holds data
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
// β
Cart handles products & total
class Cart {
constructor() {
this.items = [];
}
addItem(product, quantity) {
this.items.push({ product, quantity });
}
getTotal() {
return this.items.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
);
}
}
// β
Product is a simple data class
class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}2. O β Open/Closed Principle (OCP)
π Payment should be extendable without modifying existing code.
// Base Payment Strategy (abstract idea)
class PaymentStrategy {
pay(amount) {}
}
// New payment methods can be added without touching old ones
class CreditCardPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid $${amount} with Credit Card`);
}
}
class PayPalPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid $${amount} with PayPal`);
}
}3. L β Liskov Substitution Principle (LSP)
π Any PaymentStrategy should be usable without breaking checkout.
// Example: Adding UPI Payment (works exactly like others)
class UpiPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid $${amount} via UPI`);
}
}Here, all payment methods (CreditCardPayment, PayPalPayment, UpiPayment) behave consistently β checkout doesnβt care which one is used.
4. I β Interface Segregation Principle (ISP)
π Notifications: Donβt force checkout to use unwanted channels.
class EmailNotification {
send(to, message) {
console.log(`Email sent to ${to}: ${message}`);
}
}
class SMSNotification {
send(to, message) {
console.log(`SMS sent to ${to}: ${message}`);
}
}Now, Checkout can pick the notification type it actually needs (not forced to use both).
5. D β Dependency Inversion Principle (DIP)
π Checkout depends on abstractions (Payment + Notification), not concrete classes.
class Checkout {
constructor(paymentStrategy, notificationService) {
this.paymentStrategy = paymentStrategy; // abstraction
this.notificationService = notificationService; // abstraction
}
processOrder(user, cart) {
const amount = cart.getTotal();
// β
Pay using selected strategy
this.paymentStrategy.pay(amount);
// β
Notify using selected service
this.notificationService.send(
user.email,
`Your order of $${amount} has been placed successfully!`
);
}
}π― Usage Example
// Create a user
const user = new User("Anukool", "anukool@example.com");
// Add products
const cart = new Cart();
cart.addItem(new Product("Laptop", 1000), 1);
cart.addItem(new Product("Mouse", 50), 2);
// Choose Payment & Notification strategy
const paymentMethod = new PayPalPayment(); // can swap with CreditCardPayment or UpiPayment
const notificationService = new EmailNotification(); // can swap with SMSNotification
// Checkout
const checkout = new Checkout(paymentMethod, notificationService);
checkout.processOrder(user, cart);β How All 5 SOLID Principles Apply Here
- S (Single Responsibility): User, Cart, Payment, Notification each handle only one thing.
- O (Open/Closed): Add new payment methods (UPI, Crypto) without touching existing code.
- L (Liskov): All payment methods behave like
PaymentStrategy, so theyβre interchangeable. - I (Interface Segregation): Checkout can choose Email or SMS notifications, not forced into one big interface.
- D (Dependency Inversion): Checkout depends on abstract strategies, not specific classes.
π This way, the system is flexible, testable, and extendable.
