Hernando Abella
Chapter 1SOLIDJavaScriptDesign Patterns

Applying SOLID Principles in Modern JavaScript Applications

Learn how to write maintainable, scalable, and testable JavaScript code using the five SOLID principles — essential for modern Node.js and frontend development.

20 min read Hernando Abella📘 Node.js Design Patterns
StackJavaScriptTypeScriptNode.jsReactNestJS

Modern JavaScript applications have grown far beyond simple scripts. As applications grow, maintaining clean, scalable, and testable code becomes increasingly important.

The SOLID principles, originally introduced by Robert C. Martin (Uncle Bob), provide a foundation for creating maintainable software architectures. Although these principles originated in object-oriented programming, they remain highly relevant in modern JavaScript development.


What Is SOLID?

SOLID is an acronym representing five design principles:

SRP
Single Responsibility

A class should have only one reason to change.

OCP
Open/Closed

Open for extension, closed for modification.

LSP
Liskov Substitution

Subtypes must be substitutable for their base types.

ISP
Interface Segregation

Clients should not depend on methods they don't use.

DIP
Dependency Inversion

Depend on abstractions, not concretions.


Single Responsibility Principle (SRP)

Definition: A class, module, or function should have only one reason to change.

Bad Example

javascript · bad-example.js
1class UserService {
2  async createUser(userData) {
3    // Validate data
4    if (!userData.email) {
5      throw new Error("Email required");
6    }
7
8    // Save to database
9    await database.users.insert(userData);
10
11    // Send email
12    await emailService.sendWelcomeEmail(userData.email);
13
14    // Log action
15    console.log("User created");
16  }
17}

Better Example

javascript · good-example.js
1class UserValidator {
2  validate(user) {
3    if (!user.email) {
4      throw new Error("Email required");
5    }
6  }
7}
8
9class UserRepository {
10  async save(user) {
11    return database.users.insert(user);
12  }
13}
14
15class NotificationService {
16  async sendWelcomeEmail(email) {
17    return emailService.sendWelcomeEmail(email);
18  }
19}
20
21class UserService {
22  constructor(validator, repository, notifier) {
23    this.validator = validator;
24    this.repository = repository;
25    this.notifier = notifier;
26  }
27
28  async createUser(user) {
29    this.validator.validate(user);
30    await this.repository.save(user);
31    await this.notifier.sendWelcomeEmail(user.email);
32  }
33}

✓ Benefits: Easier testing, improved readability, reduced coupling, better maintainability


Open/Closed Principle (OCP)

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

Bad Example

javascript · bad-example.js
1function calculateDiscount(customerType, amount) {
2  if (customerType === "regular") {
3    return amount * 0.05;
4  }
5  if (customerType === "premium") {
6    return amount * 0.10;
7  }
8  if (customerType === "vip") {
9    return amount * 0.20;
10  }
11}

Better Example

javascript · good-example.js
1class DiscountStrategy {
2  calculate(amount) {
3    return 0;
4  }
5}
6
7class RegularDiscount extends DiscountStrategy {
8  calculate(amount) {
9    return amount * 0.05;
10  }
11}
12
13class PremiumDiscount extends DiscountStrategy {
14  calculate(amount) {
15    return amount * 0.10;
16  }
17}
18
19class VipDiscount extends DiscountStrategy {
20  calculate(amount) {
21    return amount * 0.20;
22  }
23}
24
25// Usage
26const strategy = new VipDiscount();
27const discount = strategy.calculate(1000);

Liskov Substitution Principle (LSP)

Definition: Subtypes must be replaceable for their base types without altering application behavior.

Bad Example

javascript · bad-example.js
1class Bird {
2  fly() {
3    console.log("Flying");
4  }
5}
6
7class Penguin extends Bird {
8  fly() {
9    throw new Error("Penguins cannot fly");
10  }
11}

Better Example

javascript · good-example.js
1class Bird {}
2
3class FlyingBird extends Bird {
4  fly() {
5    console.log("Flying");
6  }
7}
8
9class Eagle extends FlyingBird {}
10
11class Penguin extends Bird {}
12
13// Now Penguin can be used anywhere Bird is expected

Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on methods they do not use.

javascript · good-example.js
1class Workable {
2  work() {}
3}
4
5class Eatable {
6  eat() {}
7}
8
9class Developer extends Workable {
10  work() {
11    console.log("Writing code");
12  }
13}
14
15class Human extends Eatable {
16  eat() {
17    console.log("Eating");
18  }
19}
20
21// Each class depends only on behaviors it actually needs

Dependency Inversion Principle (DIP)

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

Bad Example

javascript · bad-example.js
1class MySQLDatabase {
2  save(data) {
3    console.log("Saving to MySQL");
4  }
5}
6
7class UserService {
8  constructor() {
9    this.database = new MySQLDatabase();
10  }
11
12  createUser(user) {
13    this.database.save(user);
14  }
15}

Better Example

javascript · good-example.js
1class UserService {
2  constructor(database) {
3    this.database = database;
4  }
5
6  createUser(user) {
7    this.database.save(user);
8  }
9}
10
11class MySQLDatabase {
12  save(data) {
13    console.log("MySQL save");
14  }
15}
16
17class MongoDatabase {
18  save(data) {
19    console.log("MongoDB save");
20  }
21}
22
23// Usage - easy to switch implementations
24const database = new MongoDatabase();
25const userService = new UserService(database);

✓ Benefits: Easier testing, better flexibility, improved maintainability, simplified dependency injection


SOLID in Modern JavaScript Frameworks

React

  • Components follow SRP
  • Hooks separate concerns
  • Context promotes dependency inversion

Node.js

  • Services and repositories support SRP
  • Middleware encourages OCP
  • Dependency injection supports DIP

NestJS

  • Modules isolate responsibilities
  • Providers use dependency injection
  • Interfaces encourage loose coupling

TypeScript

  • Interfaces make abstractions explicit
  • Strong typing improves maintainability
  • Better refactoring support

Common Mistakes When Applying SOLID

  • ⚠️ Creating unnecessary abstractions
  • ⚠️ Building deep inheritance trees
  • ⚠️ Overusing interfaces
  • ⚠️ Introducing complexity too early

SOLID should solve real design problems rather than serve as a rigid set of rules.


Practical Example: Refactoring a Node.js Service

Before SOLID

javascript · before.js
1class OrderService {
2  async createOrder(order) {
3    validate(order);
4    saveToDatabase(order);
5    sendEmail(order);
6    processPayment(order);
7  }
8}

After Applying SOLID

javascript · after.js
1class OrderService {
2  constructor(
3    validator,
4    repository,
5    paymentProcessor,
6    notificationService
7  ) {
8    this.validator = validator;
9    this.repository = repository;
10    this.paymentProcessor = paymentProcessor;
11    this.notificationService = notificationService;
12  }
13
14  async createOrder(order) {
15    this.validator.validate(order);
16    await this.paymentProcessor.process(order);
17    await this.repository.save(order);
18    await this.notificationService.notify(order);
19  }
20}

✓ The result is a system that is easier to test, extend, and maintain.


Conclusion

SOLID principles remain highly relevant in modern JavaScript development. Whether you're building React applications, Node.js APIs, microservices, or enterprise systems, these principles provide a practical framework for writing maintainable software.

The key goal of SOLID is not to increase complexity but to create code that adapts gracefully to change. By applying these principles thoughtfully, developers can build JavaScript applications that remain scalable and manageable as requirements evolve.

Mastering SOLID is one of the most valuable investments a JavaScript developer can make for long-term code quality and architectural success.


📘 From the Book

Node.js Design Patterns

Master SOLID principles, design patterns, and best practices for building scalable Node.js applications. Includes real-world examples and production-ready code.

🎯 SOLID Principles🏗️ Design Patterns⚡ Performance🔧 Best Practices
Get it on Amazon →
Node.js Design Patterns book cover
Share X LinkedIn
Hernando Abella

Hernando Abella

Software engineer and author. I write about Python, AI, and software architecture. Author of 55+ programming books and creator of interactive coding challenges.