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:
A class should have only one reason to change.
Open for extension, closed for modification.
Subtypes must be substitutable for their base types.
Clients should not depend on methods they don't use.
Depend on abstractions, not concretions.
Single Responsibility Principle (SRP)
Definition: A class, module, or function should have only one reason to change.
Bad Example
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
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
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
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
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
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 expectedInterface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on methods they do not use.
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 needsDependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example
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
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
1class OrderService {
2 async createOrder(order) {
3 validate(order);
4 saveToDatabase(order);
5 sendEmail(order);
6 processPayment(order);
7 }
8}After Applying SOLID
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.
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.



