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
class UserService {
async createUser(userData) {
// Validate data
if (!userData.email) {
throw new Error("Email required");
}
// Save to database
await database.users.insert(userData);
// Send email
await emailService.sendWelcomeEmail(userData.email);
// Log action
console.log("User created");
}
}Better Example
class UserValidator {
validate(user) {
if (!user.email) {
throw new Error("Email required");
}
}
}
class UserRepository {
async save(user) {
return database.users.insert(user);
}
}
class NotificationService {
async sendWelcomeEmail(email) {
return emailService.sendWelcomeEmail(email);
}
}
class UserService {
constructor(validator, repository, notifier) {
this.validator = validator;
this.repository = repository;
this.notifier = notifier;
}
async createUser(user) {
this.validator.validate(user);
await this.repository.save(user);
await this.notifier.sendWelcomeEmail(user.email);
}
}✓ 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
function calculateDiscount(customerType, amount) {
if (customerType === "regular") {
return amount * 0.05;
}
if (customerType === "premium") {
return amount * 0.10;
}
if (customerType === "vip") {
return amount * 0.20;
}
}Better Example
class DiscountStrategy {
calculate(amount) {
return 0;
}
}
class RegularDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.05;
}
}
class PremiumDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.10;
}
}
class VipDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.20;
}
}
// Usage
const strategy = new VipDiscount();
const discount = strategy.calculate(1000);Liskov Substitution Principle (LSP)
Definition: Subtypes must be replaceable for their base types without altering application behavior.
Bad Example
class Bird {
fly() {
console.log("Flying");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Penguins cannot fly");
}
}Better Example
class Bird {}
class FlyingBird extends Bird {
fly() {
console.log("Flying");
}
}
class Eagle extends FlyingBird {}
class Penguin extends Bird {}
// 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.
class Workable {
work() {}
}
class Eatable {
eat() {}
}
class Developer extends Workable {
work() {
console.log("Writing code");
}
}
class Human extends Eatable {
eat() {
console.log("Eating");
}
}
// 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
class MySQLDatabase {
save(data) {
console.log("Saving to MySQL");
}
}
class UserService {
constructor() {
this.database = new MySQLDatabase();
}
createUser(user) {
this.database.save(user);
}
}Better Example
class UserService {
constructor(database) {
this.database = database;
}
createUser(user) {
this.database.save(user);
}
}
class MySQLDatabase {
save(data) {
console.log("MySQL save");
}
}
class MongoDatabase {
save(data) {
console.log("MongoDB save");
}
}
// Usage - easy to switch implementations
const database = new MongoDatabase();
const 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
class OrderService {
async createOrder(order) {
validate(order);
saveToDatabase(order);
sendEmail(order);
processPayment(order);
}
}After Applying SOLID
class OrderService {
constructor(
validator,
repository,
paymentProcessor,
notificationService
) {
this.validator = validator;
this.repository = repository;
this.paymentProcessor = paymentProcessor;
this.notificationService = notificationService;
}
async createOrder(order) {
this.validator.validate(order);
await this.paymentProcessor.process(order);
await this.repository.save(order);
await this.notificationService.notify(order);
}
}✓ 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.
