Insightsby Anukool
HomeArticlesPortfolio β†—

Β© 2026 Anukool Patel. All rights reserved.

HomeArticlesPortfolio β†—
Back to articles

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:

A
Anukool Patel
July 20, 2026
6 min read
πŸš€ What are SOLID Principles?

SOLID is a set of five design principles that help us write better, cleaner, and more maintainable code. They are:

  1. S β€” Single Responsibility Principle (SRP)
  2. O β€” Open/Closed Principle (OCP)
  3. L β€” Liskov Substitution Principle (LSP)
  4. I β€” Interface Segregation Principle (ISP)
  5. 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:

  • User only holds data.
  • UserRepository handles saving.
  • EmailService handles 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.

#SOLID#Solid Principles#System Design Concepts#Software Development
A

Anukool Patel

Full Stack Developer

Writing about full-stack engineering, backend systems, and modern web development. See portfolio β†—