Skip to main content

SOLID Principles in JavaScript

Created: May 8, 2026 Larry Qu 7 min read

SOLID principles are fundamental design guidelines for writing maintainable, scalable code. This article covers all five principles with practical JavaScript examples.

Introduction

SOLID principles provide:

  • Code maintainability
  • Scalability
  • Flexibility
  • Testability
  • Reduced coupling

Understanding SOLID helps you:

  • Write better code
  • Design flexible systems
  • Reduce technical debt
  • Improve collaboration
  • Build sustainable projects

Single Responsibility Principle (SRP)

Definition

A class should have only one reason to change. Each class should have a single responsibility.

Bad Example

// ❌ Bad: Multiple responsibilities
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  // Responsibility 1: User data
  getUser() {
    return { name: this.name, email: this.email };
  }

  // Responsibility 2: Database operations
  saveToDatabase() {
    // Save to database
  }

  // Responsibility 3: Email sending
  sendWelcomeEmail() {
    // Send email
  }

  // Responsibility 4: Logging
  logUserCreation() {
    console.log(`User ${this.name} created`);
  }
}

Good Example

// ✅ Good: Single responsibility per class
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  getUser() {
    return { name: this.name, email: this.email };
  }
}

class UserRepository {
  save(user) {
    // Save to database
  }

  findById(id) {
    // Find user by ID
  }
}

class EmailService {
  sendWelcomeEmail(user) {
    // Send welcome email
  }
}

class Logger {
  logUserCreation(user) {
    console.log(`User ${user.name} created`);
  }
}

// Usage
const user = new User('John', '[email protected]');
const repository = new UserRepository();
const emailService = new EmailService();
const logger = new Logger();

repository.save(user);
emailService.sendWelcomeEmail(user);
logger.logUserCreation(user);

Open/Closed Principle (OCP)

Definition

Software entities should be open for extension but closed for modification.

Bad Example

// ❌ Bad: Requires modification to extend
class PaymentProcessor {
  processPayment(payment) {
    if (payment.type === 'credit_card') {
      // Process credit card
    } else if (payment.type === 'paypal') {
      // Process PayPal
    } else if (payment.type === 'bitcoin') {
      // Process Bitcoin
    }
    // Adding new payment type requires modifying this class
  }
}

Good Example

// ✅ Good: Open for extension, closed for modification
class PaymentProcessor {
  constructor(strategy) {
    this.strategy = strategy;
  }

  processPayment(amount) {
    return this.strategy.process(amount);
  }
}

class CreditCardPayment {
  process(amount) {
    console.log(`Processing credit card payment: $${amount}`);
    return { success: true, amount };
  }
}

class PayPalPayment {
  process(amount) {
    console.log(`Processing PayPal payment: $${amount}`);
    return { success: true, amount };
  }
}

class BitcoinPayment {
  process(amount) {
    console.log(`Processing Bitcoin payment: ${amount} BTC`);
    return { success: true, amount };
  }
}

// Usage
const creditCardProcessor = new PaymentProcessor(new CreditCardPayment());
creditCardProcessor.processPayment(100);

const paypalProcessor = new PaymentProcessor(new PayPalPayment());
paypalProcessor.processPayment(100);

// Adding new payment type doesn't require modifying PaymentProcessor
class ApplePayPayment {
  process(amount) {
    console.log(`Processing Apple Pay payment: $${amount}`);
    return { success: true, amount };
  }
}

const applePayProcessor = new PaymentProcessor(new ApplePayPayment());
applePayProcessor.processPayment(100);

Liskov Substitution Principle (LSP)

Definition

Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.

Bad Example

// ❌ Bad: Violates LSP
class Bird {
  fly() {
    return 'Flying';
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error('Penguins cannot fly');
  }
}

// This breaks LSP because Penguin can't be used where Bird is expected
function makeBirdFly(bird) {
  return bird.fly(); // Fails for Penguin
}

Good Example

// ✅ Good: Respects LSP
class Bird {
  move() {
    // Base movement
  }
}

class FlyingBird extends Bird {
  move() {
    return 'Flying';
  }
}

class SwimmingBird extends Bird {
  move() {
    return 'Swimming';
  }
}

class Penguin extends SwimmingBird {
  move() {
    return 'Swimming and waddling';
  }
}

class Eagle extends FlyingBird {
  move() {
    return 'Flying high';
  }
}

// Now any Bird can be used interchangeably
function makeBirdMove(bird) {
  return bird.move(); // Works for all birds
}

console.log(makeBirdMove(new Penguin())); // 'Swimming and waddling'
console.log(makeBirdMove(new Eagle())); // 'Flying high'

Interface Segregation Principle (ISP)

Definition

Clients should not be forced to depend on interfaces they don’t use.

Bad Example

// ❌ Bad: Fat interface
class Worker {
  work() { }
  eat() { }
  sleep() { }
}

class Robot extends Worker {
  work() {
    return 'Working';
  }

  eat() {
    throw new Error('Robots do not eat');
  }

  sleep() {
    throw new Error('Robots do not sleep');
  }
}

// Robot is forced to implement methods it doesn't need

Good Example

// ✅ Good: Segregated interfaces
class Workable {
  work() { }
}

class Eatable {
  eat() { }
}

class Sleepable {
  sleep() { }
}

class Human extends Workable {
  work() {
    return 'Working';
  }
}

Object.assign(Human.prototype, new Eatable(), new Sleepable());

class Robot extends Workable {
  work() {
    return 'Working';
  }
}

// Robot only implements what it needs
const human = new Human();
console.log(human.work()); // 'Working'

const robot = new Robot();
console.log(robot.work()); // 'Working'

Dependency Inversion Principle (DIP)

Definition

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Bad Example

// ❌ Bad: High-level depends on low-level
class MySQLDatabase {
  save(data) {
    console.log('Saving to MySQL:', data);
  }
}

class UserService {
  constructor() {
    this.database = new MySQLDatabase(); // Direct dependency
  }

  createUser(user) {
    this.database.save(user);
  }
}

// Changing database requires modifying UserService

Good Example

// ✅ Good: Both depend on abstraction
class UserService {
  constructor(database) {
    this.database = database; // Injected dependency
  }

  createUser(user) {
    this.database.save(user);
  }
}

class MySQLDatabase {
  save(data) {
    console.log('Saving to MySQL:', data);
  }
}

class MongoDBDatabase {
  save(data) {
    console.log('Saving to MongoDB:', data);
  }
}

// Usage
const mysqlDb = new MySQLDatabase();
const userService1 = new UserService(mysqlDb);
userService1.createUser({ name: 'John' });

const mongoDb = new MongoDBDatabase();
const userService2 = new UserService(mongoDb);
userService2.createUser({ name: 'Jane' });

// Easy to switch databases without modifying UserService

Practical SOLID Examples

E-commerce System

// ✅ Good: SOLID e-commerce system
// SRP: Each class has one responsibility
class Product {
  constructor(id, name, price) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
}

class ShoppingCart {
  constructor() {
    this.items = [];
  }

  addItem(product) {
    this.items.push(product);
  }

  getTotal() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

// OCP: Open for extension
class PaymentStrategy {
  process(amount) {
    throw new Error('Must implement process method');
  }
}

class CreditCardPayment extends PaymentStrategy {
  process(amount) {
    console.log(`Processing credit card: $${amount}`);
    return true;
  }
}

class PayPalPayment extends PaymentStrategy {
  process(amount) {
    console.log(`Processing PayPal: $${amount}`);
    return true;
  }
}

// DIP: Depends on abstraction
class Order {
  constructor(cart, paymentStrategy) {
    this.cart = cart;
    this.paymentStrategy = paymentStrategy;
  }

  checkout() {
    const total = this.cart.getTotal();
    return this.paymentStrategy.process(total);
  }
}

// Usage
const cart = new ShoppingCart();
cart.addItem(new Product(1, 'Laptop', 999));
cart.addItem(new Product(2, 'Mouse', 29));

const payment = new CreditCardPayment();
const order = new Order(cart, payment);
order.checkout(); // Processing credit card: $1028

Notification System

// ✅ Good: SOLID notification system
// SRP: Each notifier has one responsibility
class EmailNotifier {
  send(recipient, message) {
    console.log(`Email to ${recipient}: ${message}`);
  }
}

class SMSNotifier {
  send(recipient, message) {
    console.log(`SMS to ${recipient}: ${message}`);
  }
}

class SlackNotifier {
  send(recipient, message) {
    console.log(`Slack to ${recipient}: ${message}`);
  }
}

// ISP: Segregated interfaces
class NotificationService {
  constructor(notifiers = []) {
    this.notifiers = notifiers;
  }

  addNotifier(notifier) {
    this.notifiers.push(notifier);
  }

  notify(recipient, message) {
    this.notifiers.forEach(notifier => {
      notifier.send(recipient, message);
    });
  }
}

// Usage
const service = new NotificationService();
service.addNotifier(new EmailNotifier());
service.addNotifier(new SMSNotifier());
service.addNotifier(new SlackNotifier());

service.notify('[email protected]', 'Hello!');
// Email to [email protected]: Hello!
// SMS to [email protected]: Hello!
// Slack to [email protected]: Hello!

Best Practices

  1. Apply SRP: One responsibility per class:
    // ✅ Good
    class User { }
    class UserRepository { }
    class UserValidator { }
    
    // ❌ Bad
    class User {
      save() { }
      validate() { }
    }
    ```javascript
    
  2. Use dependency injection:
    // ✅ Good
    class Service {
      constructor(dependency) {
        this.dependency = dependency;
      }
    }
    
    // ❌ Bad
    class Service {
      constructor() {
        this.dependency = new Dependency();
      }
    }
    ```javascript
    
  3. Program to interfaces, not implementations:
    // ✅ Good
    class Service {
      constructor(repository) {
        this.repository = repository;
      }
    }
    
    // ❌ Bad
    class Service {
      constructor() {
        this.repository = new MySQLRepository();
      }
    }
    ```javascript
    

Common Mistakes

  1. Violating SRP by mixing concerns:
    // ❌ Bad
    class User {
      save() { }
      sendEmail() { }
      logActivity() { }
    }
    
    // ✅ Good
    class User { }
    class UserRepository { save() { } }
    class EmailService { sendEmail() { } }
    class Logger { logActivity() { } }
    ```javascript
    
  2. Tight coupling:
    // ❌ Bad
    class Service {
      constructor() {
        this.db = new Database();
      }
    }
    
    // ✅ Good
    class Service {
      constructor(db) {
        this.db = db;
      }
    }
    

Summary

SOLID principles guide sustainable code design. Key takeaways:

  • Single Responsibility: One reason to change
  • Open/Closed: Extend without modifying
  • Liskov Substitution: Substitutable implementations
  • Interface Segregation: Specific interfaces
  • Dependency Inversion: Depend on abstractions
  • Improves maintainability
  • Reduces coupling
  • Enables testing

Next Steps

Resources

Comments

Share this article

Scan to read on mobile