SOLID Principles in JavaScript: Complete Guide to Better Software Design
Learn how to apply SOLID principles in JavaScript development. complete guide with practical examples covering Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles.
Amr S.
Author & Developer

SOLID Principles in JavaScript: Complete Guide to Better Software Design
SOLID is an acronym representing five fundamental design principles that help developers create more maintainable, flexible, and scalable software. These principles, introduced by Robert C. Martin (Uncle Bob), form the foundation of clean code and good software engineering.
While SOLID principles originated in object-oriented programming, they translate beautifully to JavaScript development, helping you write better code whether you're building frontend applications, backend services, or full-stack solutions.
💡 SOLID stands for: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles.
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change. In other words, each class should have only one job or responsibility.
Bad Example: Violating SRP
// BAD: User class has too many responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// Responsibility 1: User data management
getName() {
return this.name;
}
setName(name) {
this.name = name;
}
// Responsibility 2: Email validation
validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(this.email);
}
// Responsibility 3: Database operations
save() {
if (!this.validateEmail()) {
throw new Error('Invalid email');
}
// Database save logic
console.log(`Saving user ${this.name} to database`);
}
// Responsibility 4: Email sending
sendWelcomeEmail() {
if (!this.validateEmail()) {
throw new Error('Invalid email');
}
// Email sending logic
console.log(`Sending welcome email to ${this.email}`);
}
// Responsibility 5: User reporting
generateReport() {
return `User Report: ${this.name} (${this.email})`;
}
}
// This class has 5 different reasons to change:
// 1. User data structure changes
// 2. Email validation rules change
// 3. Database schema changes
// 4. Email service changes
// 5. Report format changes
Good Example: Following SRP
// GOOD: Each class has a single responsibility
// Responsibility 1: User data management
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
setName(name) {
this.name = name;
}
getEmail() {
return this.email;
}
setEmail(email) {
this.email = email;
}
}
// Responsibility 2: Email validation
class EmailValidator {
static validate(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static validateAndThrow(email) {
if (!this.validate(email)) {
throw new Error('Invalid email format');
}
}
}
// Responsibility 3: Database operations
class UserRepository {
save(user) {
EmailValidator.validateAndThrow(user.getEmail());
// Database save logic
console.log(`Saving user ${user.getName()} to database`);
return { id: Date.now(), ...user };
}
findById(id) {
// Database retrieval logic
console.log(`Finding user with ID: ${id}`);
return new User('John Doe', 'john@example.com');
}
update(id, userData) {
// Database update logic
console.log(`Updating user ${id} with data:, userData`);
}
delete(id) {
// Database deletion logic
console.log(`Deleting user with ID: ${id}`);
}
}
// Responsibility 4: Email sending
class EmailService {
sendWelcomeEmail(user) {
EmailValidator.validateAndThrow(user.getEmail());
// Email sending logic
console.log(`Sending welcome email to ${user.getEmail()}`);
}
sendPasswordReset(user) {
EmailValidator.validateAndThrow(user.getEmail());
console.log(`Sending password reset email to ${user.getEmail()}`);
}
}
// Responsibility 5: User reporting
class UserReportGenerator {
generateBasicReport(user) {
return `User Report: ${user.getName()} (${user.getEmail()})`;
}
generateDetailedReport(user, additionalData) {
return `
Detailed User Report
Name: ${user.getName()}
Email: ${user.getEmail()}
Created: ${additionalData.createdAt}
Last Login: ${additionalData.lastLogin}
`;
}
}
// Usage
const user = new User('Jane Smith', 'jane@example.com');
const userRepo = new UserRepository();
const emailService = new EmailService();
const reportGenerator = new UserReportGenerator();
const savedUser = userRepo.save(user);
emailService.sendWelcomeEmail(user);
const report = reportGenerator.generateBasicReport(user);
console.log(report);
2. Open/Closed Principle (OCP)
The Open/Closed Principle states that software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
Bad Example: Violating OCP
// BAD: Need to modify existing code to add new shapes
class AreaCalculator {
calculateArea(shapes) {
let totalArea = 0;
shapes.forEach(shape => {
if (shape.type === 'rectangle') {
totalArea += shape.width * shape.height;
} else if (shape.type === 'circle') {
totalArea += Math.PI * shape.radius * shape.radius;
} else if (shape.type === 'triangle') {
// Adding triangle requires modifying this class!
totalArea += (shape.base * shape.height) / 2;
}
// Adding new shapes requires modifying this method!
});
return totalArea;
}
}
// Adding new shapes requires modification of the AreaCalculator class
const shapes = [
{ type: 'rectangle', width: 10, height: 5 },
{ type: 'circle', radius: 3 },
{ type: 'triangle', base: 4, height: 6 }
];
const calculator = new AreaCalculator();
console.log(calculator.calculateArea(shapes)); // 105.28...
Good Example: Following OCP
// GOOD: Open for extension, closed for modification
// Base shape interface
class Shape {
calculateArea() {
throw new Error('calculateArea method must be implemented');
}
}
// Concrete shape implementations
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class Triangle extends Shape {
constructor(base, height) {
super();
this.base = base;
this.height = height;
}
calculateArea() {
return (this.base * this.height) / 2;
}
}
// Calculator that doesn't need modification
class AreaCalculator {
calculateArea(shapes) {
return shapes.reduce((totalArea, shape) => {
return totalArea + shape.calculateArea();
}, 0);
}
calculatePerimeter(shapes) {
return shapes.reduce((totalPerimeter, shape) => {
return totalPerimeter + (shape.calculatePerimeter ? shape.calculatePerimeter() : 0);
}, 0);
}
}
// Adding new shapes without modifying existing code
class Pentagon extends Shape {
constructor(side, apothem) {
super();
this.side = side;
this.apothem = apothem;
}
calculateArea() {
return (5 * this.side * this.apothem) / 2;
}
}
class Hexagon extends Shape {
constructor(side) {
super();
this.side = side;
}
calculateArea() {
return (3 * Math.sqrt(3) * this.side * this.side) / 2;
}
}
// Usage - no changes needed to AreaCalculator
const shapes = [
new Rectangle(10, 5),
new Circle(3),
new Triangle(4, 6),
new Pentagon(5, 3.44),
new Hexagon(4)
];
const calculator = new AreaCalculator();
console.log(`Total area: ${calculator.calculateArea(shapes)}`);
// Decorator pattern example for extending functionality
class ShapeWithBorder extends Shape {
constructor(shape, borderWidth) {
super();
this.shape = shape;
this.borderWidth = borderWidth;
}
calculateArea() {
// Calculate base area plus border area
const baseArea = this.shape.calculateArea();
const borderArea = this.calculateBorderArea();
return baseArea + borderArea;
}
calculateBorderArea() {
// Simplified border area calculation
return this.borderWidth * 10; // Example calculation
}
}
// Extending without modifying existing classes
const rectangleWithBorder = new ShapeWithBorder(new Rectangle(10, 5), 2);
console.log(`Rectangle with border area: ${rectangleWithBorder.calculateArea()}`);
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subtypes must be substitutable for their base types.
Bad Example: Violating LSP
// BAD: LSP violation - Square changes behavior of Rectangle
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(side) {
super(side, side);
}
// LSP Violation: Changes expected behavior
setWidth(width) {
this.width = width;
this.height = width; // Side effect!
}
setHeight(height) {
this.width = height; // Side effect!
this.height = height;
}
}
// This function expects Rectangle behavior
function increaseRectangleWidth(rectangle) {
const originalHeight = rectangle.height;
rectangle.setWidth(rectangle.width + 1);
// This assertion will fail for Square!
console.log(`Original height: ${originalHeight}, Current height: ${rectangle.height}`);
return rectangle.height === originalHeight; // Expected to be true for Rectangle
}
// Usage
const rectangle = new Rectangle(5, 4);
const square = new Square(5);
console.log('Rectangle test:', increaseRectangleWidth(rectangle)); // true
console.log('Square test:', increaseRectangleWidth(square)); // false - LSP violation!
Good Example: Following LSP
// GOOD: LSP compliant design
// Base shape class with common interface
class Shape {
getArea() {
throw new Error('getArea method must be implemented');
}
getPerimeter() {
throw new Error('getPerimeter method must be implemented');
}
}
// Rectangle with its own specific behavior
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
getPerimeter() {
return 2 * (this.width + this.height);
}
}
// Square as independent shape, not inheriting from Rectangle
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
setSide(side) {
this.side = side;
}
getArea() {
return this.side * this.side;
}
getPerimeter() {
return 4 * this.side;
}
}
// Circle for demonstration
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
setRadius(radius) {
this.radius = radius;
}
getArea() {
return Math.PI * this.radius * this.radius;
}
getPerimeter() {
return 2 * Math.PI * this.radius;
}
}
// Functions that work with any Shape (LSP compliant)
function calculateTotalArea(shapes) {
return shapes.reduce((total, shape) => total + shape.getArea(), 0);
}
function printShapeInfo(shape) {
console.log(`Area: ${shape.getArea().toFixed(2)}, Perimeter: ${shape.getPerimeter().toFixed(2)}`);
}
function scaleShapes(shapes, factor) {
shapes.forEach(shape => {
if (shape instanceof Rectangle) {
shape.setWidth(shape.width * factor);
shape.setHeight(shape.height * factor);
} else if (shape instanceof Square) {
shape.setSide(shape.side * factor);
} else if (shape instanceof Circle) {
shape.setRadius(shape.radius * factor);
}
});
}
// Usage - all shapes are interchangeable
const shapes = [
new Rectangle(5, 4),
new Square(5),
new Circle(3)
];
console.log('Original shapes:');
shapes.forEach(shape => printShapeInfo(shape));
console.log(`Total area: ${calculateTotalArea(shapes).toFixed(2)}`);
scaleShapes(shapes, 2);
console.log('\nScaled shapes:');
shapes.forEach(shape => printShapeInfo(shape));
// Better approach using composition for specific behaviors
class ResizableRectangle {
constructor(width, height) {
this.shape = new Rectangle(width, height);
}
resize(widthFactor, heightFactor) {
this.shape.setWidth(this.shape.width * widthFactor);
this.shape.setHeight(this.shape.height * heightFactor);
}
getArea() {
return this.shape.getArea();
}
getPerimeter() {
return this.shape.getPerimeter();
}
}
class ResizableSquare {
constructor(side) {
this.shape = new Square(side);
}
resize(factor) {
this.shape.setSide(this.shape.side * factor);
}
getArea() {
return this.shape.getArea();
}
getPerimeter() {
return this.shape.getPerimeter();
}
}
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. It's better to have many specific interfaces than one general-purpose interface.
Bad Example: Violating ISP
// BAD: Fat interface forces classes to implement unused methods
class Animal {
eat() { throw new Error('Must implement eat'); }
sleep() { throw new Error('Must implement sleep'); }
fly() { throw new Error('Must implement fly'); }
swim() { throw new Error('Must implement swim'); }
walk() { throw new Error('Must implement walk'); }
}
class Dog extends Animal {
eat() {
console.log('Dog is eating');
}
sleep() {
console.log('Dog is sleeping');
}
walk() {
console.log('Dog is walking');
}
// Forced to implement methods that don't make sense for a dog
fly() {
throw new Error('Dogs cannot fly!');
}
swim() {
// Some dogs can swim, but not all
throw new Error('This dog cannot swim!');
}
}
class Fish extends Animal {
eat() {
console.log('Fish is eating');
}
sleep() {
console.log('Fish is resting');
}
swim() {
console.log('Fish is swimming');
}
// Forced to implement methods that don't apply to fish
fly() {
throw new Error('Fish cannot fly!');
}
walk() {
throw new Error('Fish cannot walk!');
}
}
// This creates problems when using the animals
function makeAnimalFly(animal) {
try {
animal.fly(); // Will throw errors for most animals
} catch (error) {
console.log(error.message);
}
}
const dog = new Dog();
const fish = new Fish();
makeAnimalFly(dog); // "Dogs cannot fly!"
makeAnimalFly(fish); // "Fish cannot fly!"
Good Example: Following ISP
// GOOD: Segregated interfaces - animals only implement what they can do
// Basic interfaces
class Eater {
eat() { throw new Error('Must implement eat'); }
}
class Sleeper {
sleep() { throw new Error('Must implement sleep'); }
}
// Movement interfaces
class Walker {
walk() { throw new Error('Must implement walk'); }
}
class Swimmer {
swim() { throw new Error('Must implement swim'); }
}
class Flyer {
fly() { throw new Error('Must implement fly'); }
}
// Specific behavior interfaces
class Hunter {
hunt() { throw new Error('Must implement hunt'); }
}
class SocialAnimal {
pack() { throw new Error('Must implement pack behavior'); }
}
// Using mixins for multiple interface implementation
function applyMixins(derivedCtor, baseCtors) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
if (name !== 'constructor') {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
);
}
});
});
}
// Dog implements only relevant interfaces
class Dog {
constructor(name, breed) {
this.name = name;
this.breed = breed;
}
eat() {
console.log(`${this.name} the dog is eating`);
}
sleep() {
console.log(`${this.name} the dog is sleeping`);
}
walk() {
console.log(`${this.name} the dog is walking`);
}
hunt() {
console.log(`${this.name} the dog is hunting`);
}
pack() {
console.log(`${this.name} the dog is with its pack`);
}
}
// Apply only relevant interfaces
applyMixins(Dog, [Eater, Sleeper, Walker, Hunter, SocialAnimal]);
// Fish implements only what it can do
class Fish {
constructor(species) {
this.species = species;
}
eat() {
console.log(`The ${this.species} is eating`);
}
sleep() {
console.log(`The ${this.species} is resting`);
}
swim() {
console.log(`The ${this.species} is swimming`);
}
hunt() {
console.log(`The ${this.species} is hunting for food`);
}
}
applyMixins(Fish, [Eater, Sleeper, Swimmer, Hunter]);
// Bird implements flying, walking, and other behaviors
class Bird {
constructor(species, canFly = true) {
this.species = species;
this.canFly = canFly;
}
eat() {
console.log(`The ${this.species} is eating seeds`);
}
sleep() {
console.log(`The ${this.species} is roosting`);
}
walk() {
console.log(`The ${this.species} is walking`);
}
fly() {
if (this.canFly) {
console.log(`The ${this.species} is flying`);
} else {
throw new Error(`The ${this.species} cannot fly`);
}
}
hunt() {
console.log(`The ${this.species} is hunting for prey`);
}
}
applyMixins(Bird, [Eater, Sleeper, Walker, Flyer, Hunter]);
// Now we can have specific functions for specific capabilities
function feedAnimals(eaters) {
eaters.forEach(eater => eater.eat());
}
function exerciseWalkers(walkers) {
walkers.forEach(walker => walker.walk());
}
function watchFlyers(flyers) {
flyers.forEach(flyer => {
try {
flyer.fly();
} catch (error) {
console.log(error.message);
}
});
}
function organizeHunt(hunters) {
console.log('Organizing hunting expedition:');
hunters.forEach(hunter => hunter.hunt());
}
// Usage
const dog = new Dog('Rex', 'German Shepherd');
const fish = new Fish('Goldfish');
const eagle = new Bird('Eagle');
const penguin = new Bird('Penguin', false);
const allAnimals = [dog, fish, eagle, penguin];
const eaters = allAnimals; // All animals can eat
const walkers = [dog, eagle, penguin]; // Only land animals
const flyers = [eagle, penguin]; // Birds (though penguin can't fly)
const hunters = [dog, fish, eagle]; // Animals that hunt
console.log('=== Feeding Time ===');
feedAnimals(eaters);
console.log('\n=== Exercise Time ===');
exerciseWalkers(walkers);
console.log('\n=== Flight Time ===');
watchFlyers(flyers);
console.log('\n=== Hunting Time ===');
organizeHunt(hunters);
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.
Bad Example: Violating DIP
// BAD: High-level class depends on low-level concrete classes
// Low-level modules (concrete implementations)
class MySQLDatabase {
save(data) {
console.log('Saving to MySQL database:', data);
// MySQL-specific implementation
}
find(id) {
console.log(`Finding record ${id} from MySQL`);
return { id, data: 'MySQL data' };
}
}
class EmailService {
send(to, subject, body) {
console.log(`Sending email to ${to}: ${subject}`);
// Email service specific implementation
}
}
class FileLogger {
log(message) {
console.log(`Logging to file: ${message}`);
// File logging specific implementation
}
}
// High-level module directly depends on concrete classes
class UserService {
constructor() {
// Direct dependencies on concrete classes
this.database = new MySQLDatabase();
this.emailService = new EmailService();
this.logger = new FileLogger();
}
createUser(userData) {
try {
// Validation logic
if (!userData.email) {
throw new Error('Email is required');
}
// Save user
const savedUser = this.database.save(userData);
// Send welcome email
this.emailService.send(
userData.email,
'Welcome!',
'Welcome to my platform!'
);
// Log the action
this.logger.log(`User created: ${userData.email}`);
return savedUser;
} catch (error) {
this.logger.log(`Error creating user: ${error.message}`);
throw error;
}
}
}
// Problems with this approach:
// 1. Hard to test - can't mock dependencies
// 2. Tightly coupled - changing database requires changing UserService
// 3. Inflexible - can't switch implementations easily
// 4. Violates DIP - high-level depends on low-level
const userService = new UserService();
userService.createUser({ email: 'user@example.com', name: 'John' });
Good Example: Following DIP
// GOOD: Depend on abstractions, not concretions
// Abstractions (interfaces)
class DatabaseInterface {
save(data) { throw new Error('save method must be implemented'); }
find(id) { throw new Error('find method must be implemented'); }
update(id, data) { throw new Error('update method must be implemented'); }
delete(id) { throw new Error('delete method must be implemented'); }
}
class EmailServiceInterface {
send(to, subject, body) {
throw new Error('send method must be implemented');
}
}
class LoggerInterface {
log(message) { throw new Error('log method must be implemented'); }
error(message) { throw new Error('error method must be implemented'); }
info(message) { throw new Error('info method must be implemented'); }
}
// Concrete implementations (low-level modules)
class MySQLDatabase extends DatabaseInterface {
save(data) {
console.log('Saving to MySQL database:', data);
return { id: Date.now(), ...data };
}
find(id) {
console.log(`Finding record ${id} from MySQL`);
return { id, data: 'MySQL data' };
}
update(id, data) {
console.log(`Updating record ${id} in MySQL`);
return { id, ...data };
}
delete(id) {
console.log(`Deleting record ${id} from MySQL`);
}
}
class PostgreSQLDatabase extends DatabaseInterface {
save(data) {
console.log('Saving to PostgreSQL database:', data);
return { id: Date.now(), ...data };
}
find(id) {
console.log(`Finding record ${id} from PostgreSQL`);
return { id, data: 'PostgreSQL data' };
}
update(id, data) {
console.log(`Updating record ${id} in PostgreSQL`);
return { id, ...data };
}
delete(id) {
console.log(`Deleting record ${id} from PostgreSQL`);
}
}
class SendGridEmailService extends EmailServiceInterface {
send(to, subject, body) {
console.log(`[SendGrid] Sending to ${to}: ${subject}`);
}
}
class SMTPEmailService extends EmailServiceInterface {
send(to, subject, body) {
console.log(`[SMTP] Sending to ${to}: ${subject}`);
}
}
class FileLogger extends LoggerInterface {
log(message) {
console.log(`[FILE LOG] ${new Date().toISOString()}: ${message}`);
}
error(message) {
console.error(`[FILE ERROR] ${new Date().toISOString()}: ${message}`);
}
info(message) {
console.info(`[FILE INFO] ${new Date().toISOString()}: ${message}`);
}
}
class ConsoleLogger extends LoggerInterface {
log(message) {
console.log(`[CONSOLE] ${message}`);
}
error(message) {
console.error(`[CONSOLE ERROR] ${message}`);
}
info(message) {
console.info(`[CONSOLE INFO] ${message}`);
}
}
// High-level module depends on abstractions
class UserService {
constructor(database, emailService, logger) {
// Depend on abstractions, not concretions
if (!(database instanceof DatabaseInterface)) {
throw new Error('Database must implement DatabaseInterface');
}
if (!(emailService instanceof EmailServiceInterface)) {
throw new Error('EmailService must implement EmailServiceInterface');
}
if (!(logger instanceof LoggerInterface)) {
throw new Error('Logger must implement LoggerInterface');
}
this.database = database;
this.emailService = emailService;
this.logger = logger;
}
async createUser(userData) {
try {
this.logger.info(`Creating user: ${userData.email}`);
// Validation
if (!userData.email || !userData.name) {
throw new Error('Email and name are required');
}
// Save user (depends on abstraction)
const savedUser = this.database.save(userData);
// Send welcome email (depends on abstraction)
this.emailService.send(
userData.email,
'Welcome to My Platform!',
`Hello ${userData.name}, welcome to my amazing platform!`
);
// Log success (depends on abstraction)
this.logger.info(`User created successfully: ${savedUser.id}`);
return savedUser;
} catch (error) {
this.logger.error(`Failed to create user: ${error.message}`);
throw error;
}
}
async getUserById(id) {
try {
this.logger.info(`Fetching user: ${id}`);
const user = this.database.find(id);
return user;
} catch (error) {
this.logger.error(`Failed to fetch user ${id}: ${error.message}`);
throw error;
}
}
async updateUser(id, updates) {
try {
this.logger.info(`Updating user: ${id}`);
const updatedUser = this.database.update(id, updates);
this.logger.info(`User updated successfully: ${id}`);
return updatedUser;
} catch (error) {
this.logger.error(`Failed to update user ${id}: ${error.message}`);
throw error;
}
}
}
// Dependency Injection Container (simple implementation)
class DIContainer {
constructor() {
this.dependencies = new Map();
}
register(name, factory) {
this.dependencies.set(name, factory);
}
resolve(name) {
const factory = this.dependencies.get(name);
if (!factory) {
throw new Error(`Dependency ${name} not registered`);
}
return factory();
}
}
// Usage with Dependency Injection
const container = new DIContainer();
// Register dependencies
container.register('database', () => new MySQLDatabase());
container.register('emailService', () => new SendGridEmailService());
container.register('logger', () => new FileLogger());
// Alternative configurations
container.register('testDatabase', () => new PostgreSQLDatabase());
container.register('testEmailService', () => new SMTPEmailService());
container.register('testLogger', () => new ConsoleLogger());
// Create UserService with injected dependencies
const userService = new UserService(
container.resolve('database'),
container.resolve('emailService'),
container.resolve('logger')
);
// Test UserService with different implementations
const testUserService = new UserService(
container.resolve('testDatabase'),
container.resolve('testEmailService'),
container.resolve('testLogger')
);
// Usage
userService.createUser({
email: 'john@example.com',
name: 'John Doe'
});
testUserService.createUser({
email: 'jane@example.com',
name: 'Jane Smith'
});
// Easy to test with mock implementations
class MockDatabase extends DatabaseInterface {
save(data) { return { id: 'mock-id', ...data }; }
find(id) { return { id, data: 'mock data' }; }
update(id, data) { return { id, ...data }; }
delete(id) { return true; }
}
class MockEmailService extends EmailServiceInterface {
send(to, subject, body) {
console.log(`Mock email sent to ${to}`);
}
}
class MockLogger extends LoggerInterface {
log(message) { /* silent */ }
error(message) { /* silent */ }
info(message) { /* silent */ }
}
// Test with mocks
const testService = new UserService(
new MockDatabase(),
new MockEmailService(),
new MockLogger()
);
testService.createUser({ email: 'test@example.com', name: 'Test User' });
Real-World Example: E-commerce System
Let's see how all SOLID principles work together in a real-world e-commerce system:
// Real-world E-commerce System applying all SOLID principles
// ===== SINGLE RESPONSIBILITY PRINCIPLE =====
// Each class has one reason to change
class Product {
constructor(id, name, price, category) {
this.id = id;
this.name = name;
this.price = price;
this.category = category;
}
}
class Cart {
constructor() {
this.items = [];
}
addItem(product, quantity = 1) {
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
}
removeItem(productId) {
this.items = this.items.filter(item => item.product.id !== productId);
}
getItems() {
return this.items;
}
clear() {
this.items = [];
}
}
class Order {
constructor(id, customerId, items, totalAmount) {
this.id = id;
this.customerId = customerId;
this.items = items;
this.totalAmount = totalAmount;
this.status = 'pending';
this.createdAt = new Date();
}
updateStatus(status) {
this.status = status;
}
}
// ===== OPEN/CLOSED PRINCIPLE =====
// Open for extension, closed for modification
// Base discount strategy
class DiscountStrategy {
calculateDiscount(totalAmount) {
throw new Error('calculateDiscount must be implemented');
}
}
// Concrete discount strategies (can add new ones without modifying existing code)
class NoDiscount extends DiscountStrategy {
calculateDiscount(totalAmount) {
return 0;
}
}
class PercentageDiscount extends DiscountStrategy {
constructor(percentage) {
super();
this.percentage = percentage;
}
calculateDiscount(totalAmount) {
return totalAmount * (this.percentage / 100);
}
}
class FixedAmountDiscount extends DiscountStrategy {
constructor(amount) {
super();
this.amount = amount;
}
calculateDiscount(totalAmount) {
return Math.min(this.amount, totalAmount);
}
}
class BuyTwoGetOneDiscount extends DiscountStrategy {
calculateDiscount(totalAmount, items) {
const eligibleItems = items.filter(item => item.quantity >= 3);
let discount = 0;
eligibleItems.forEach(item => {
const freeItems = Math.floor(item.quantity / 3);
discount += freeItems * item.product.price;
});
return discount;
}
}
// ===== LISKOV SUBSTITUTION PRINCIPLE =====
// Subtypes must be substitutable for their base types
class PaymentProcessor {
processPayment(amount, paymentDetails) {
throw new Error('processPayment must be implemented');
}
validatePayment(paymentDetails) {
throw new Error('validatePayment must be implemented');
}
}
class CreditCardProcessor extends PaymentProcessor {
processPayment(amount, paymentDetails) {
if (!this.validatePayment(paymentDetails)) {
throw new Error('Invalid credit card details');
}
console.log(`Processing $${amount} via Credit Card ending in ${paymentDetails.cardNumber.slice(-4)}`);
return { success: true, transactionId: 'cc-' + Date.now() };
}
validatePayment(paymentDetails) {
return paymentDetails.cardNumber && paymentDetails.expiryDate && paymentDetails.cvv;
}
}
class PayPalProcessor extends PaymentProcessor {
processPayment(amount, paymentDetails) {
if (!this.validatePayment(paymentDetails)) {
throw new Error('Invalid PayPal details');
}
console.log(`Processing $${amount} via PayPal for ${paymentDetails.email}`);
return { success: true, transactionId: 'pp-' + Date.now() };
}
validatePayment(paymentDetails) {
return paymentDetails.email && paymentDetails.password;
}
}
class BankTransferProcessor extends PaymentProcessor {
processPayment(amount, paymentDetails) {
if (!this.validatePayment(paymentDetails)) {
throw new Error('Invalid bank details');
}
console.log(`Processing $${amount} via Bank Transfer to account ${paymentDetails.accountNumber}`);
return { success: true, transactionId: 'bt-' + Date.now() };
}
validatePayment(paymentDetails) {
return paymentDetails.accountNumber && paymentDetails.routingNumber;
}
}
// ===== INTERFACE SEGREGATION PRINCIPLE =====
// Clients shouldn't depend on interfaces they don't use
// Segregated interfaces
class OrderPersistence {
save(order) { throw new Error('save must be implemented'); }
findById(id) { throw new Error('findById must be implemented'); }
}
class NotificationSender {
sendOrderConfirmation(order, customerEmail) {
throw new Error('sendOrderConfirmation must be implemented');
}
}
class InventoryManager {
checkAvailability(productId, quantity) {
throw new Error('checkAvailability must be implemented');
}
reserveItems(items) {
throw new Error('reserveItems must be implemented');
}
}
class ShippingCalculator {
calculateShipping(items, address) {
throw new Error('calculateShipping must be implemented');
}
}
// ===== DEPENDENCY INVERSION PRINCIPLE =====
// Depend on abstractions, not concretions
class OrderService {
constructor(orderRepo, notificationService, inventoryService, shippingCalculator) {
this.orderRepo = orderRepo;
this.notificationService = notificationService;
this.inventoryService = inventoryService;
this.shippingCalculator = shippingCalculator;
}
async createOrder(cart, customer, discountStrategy, paymentProcessor, paymentDetails) {
try {
// Check inventory availability
for (const item of cart.getItems()) {
const available = await this.inventoryService.checkAvailability(
item.product.id,
item.quantity
);
if (!available) {
throw new Error(`Insufficient inventory for ${item.product.name}`);
}
}
// Calculate totals
const subtotal = cart.getItems().reduce(
(sum, item) => sum + (item.product.price * item.quantity),
0
);
const discount = discountStrategy.calculateDiscount(subtotal, cart.getItems());
const shipping = await this.shippingCalculator.calculateShipping(
cart.getItems(),
customer.address
);
const total = subtotal - discount + shipping;
// Process payment
const paymentResult = await paymentProcessor.processPayment(total, paymentDetails);
if (!paymentResult.success) {
throw new Error('Payment processing failed');
}
// Reserve inventory
await this.inventoryService.reserveItems(cart.getItems());
// Create and save order
const order = new Order(
'ord-' + Date.now(),
customer.id,
cart.getItems(),
total
);
order.paymentId = paymentResult.transactionId;
order.discountAmount = discount;
order.shippingAmount = shipping;
const savedOrder = await this.orderRepo.save(order);
// Send confirmation
await this.notificationService.sendOrderConfirmation(
savedOrder,
customer.email
);
return savedOrder;
} catch (error) {
console.error('Order creation failed:', error.message);
throw error;
}
}
}
// Concrete implementations
class InMemoryOrderRepo extends OrderPersistence {
constructor() {
super();
this.orders = new Map();
}
async save(order) {
this.orders.set(order.id, order);
return order;
}
async findById(id) {
return this.orders.get(id);
}
}
class EmailNotificationService extends NotificationSender {
async sendOrderConfirmation(order, customerEmail) {
console.log(`Sending order confirmation to ${customerEmail} for order ${order.id}`);
}
}
class SimpleInventoryService extends InventoryManager {
constructor() {
super();
this.inventory = new Map([
['prod-1', 100],
['prod-2', 50],
['prod-3', 25]
]);
}
async checkAvailability(productId, quantity) {
const available = this.inventory.get(productId) || 0;
return available >= quantity;
}
async reserveItems(items) {
items.forEach(item => {
const current = this.inventory.get(item.product.id) || 0;
this.inventory.set(item.product.id, current - item.quantity);
});
}
}
class StandardShippingCalculator extends ShippingCalculator {
async calculateShipping(items, address) {
const weight = items.reduce((sum, item) => sum + item.quantity, 0);
return weight * 2.5; // $2.50 per item
}
}
// Usage Example
async function demonstrateEcommerceSystem() {
// Create dependencies
const orderRepo = new InMemoryOrderRepo();
const notificationService = new EmailNotificationService();
const inventoryService = new SimpleInventoryService();
const shippingCalculator = new StandardShippingCalculator();
// Create order service (dependency injection)
const orderService = new OrderService(
orderRepo,
notificationService,
inventoryService,
shippingCalculator
);
// Create products
const laptop = new Product('prod-1', 'Gaming Laptop', 1299.99, 'Electronics');
const mouse = new Product('prod-2', 'Wireless Mouse', 49.99, 'Accessories');
// Create cart and add items
const cart = new Cart();
cart.addItem(laptop, 1);
cart.addItem(mouse, 2);
// Customer data
const customer = {
id: 'cust-1',
email: 'customer@example.com',
address: { city: 'New York', state: 'NY', zip: '10001' }
};
// Create discount strategy
const discount = new PercentageDiscount(10); // 10% off
// Create payment processor
const paymentProcessor = new CreditCardProcessor();
const paymentDetails = {
cardNumber: '1234567890123456',
expiryDate: '12/25',
cvv: '123'
};
// Process order
try {
const order = await orderService.createOrder(
cart,
customer,
discount,
paymentProcessor,
paymentDetails
);
console.log('Order created successfully:', order.id);
console.log('Total amount:', order.totalAmount);
} catch (error) {
console.error('Order failed:', error.message);
}
}
demonstrateEcommerceSystem();
SOLID Principles Checklist
Use this checklist when reviewing your code to ensure you're following SOLID principles:
Single Responsibility Principle
- Does this class have only one reason to change?
- Can I describe what this class does in one sentence without using "and" or "or"?
- Are all methods in this class related to its primary responsibility?
- Would changing business rules require modifying multiple methods in this class?
Open/Closed Principle
- Can I add new functionality without modifying existing code?
- Am I using inheritance, composition, or strategy patterns appropriately?
- Are my classes designed to handle new requirements through extension?
- Do I have switch statements or long if-else chains that might violate OCP?
Liskov Substitution Principle
- Can I substitute any subclass for its parent class without breaking functionality?
- Do my subclasses honor the contract established by the parent class?
- Are there any overridden methods that change expected behavior?
- Do my subclasses strengthen preconditions or weaken postconditions?
Interface Segregation Principle
- Are my interfaces focused and cohesive?
- Do classes have to implement methods they don't need?
- Can I break large interfaces into smaller, more specific ones?
- Are clients forced to depend on methods they don't use?
Dependency Inversion Principle
- Do my high-level modules depend on abstractions rather than concrete implementations?
- Can I easily swap implementations without changing high-level code?
- Am I using dependency injection to provide dependencies?
- Are my abstractions independent of implementation details?
Benefits of Following SOLID Principles
- Improved Maintainability: Code is easier to understand, modify, and debug
- Enhanced Testability: Each component can be tested in isolation with clear dependencies
- Better Flexibility: Easy to add new features and modify existing ones
- Reduced Coupling: Components are loosely connected, making changes safer
- Increased Reusability: Well-designed components can be reused across projects
- Cleaner Architecture: Overall system design becomes more organized and logical
Common Pitfalls and How to Avoid Them
⚠️ Don't over-engineer! SOLID principles should improve your code, not make it unnecessarily complex. Apply them judiciously based on your project's needs.
- Over-abstraction: Don't create abstractions for everything; focus on areas that are likely to change
- Premature optimization: Apply SOLID principles when complexity justifies them, not preemptively
- Ignoring context: Consider your team size, project timeline, and maintenance requirements
- All-or-nothing approach: You can apply principles incrementally and selectively
Conclusion
SOLID principles provide a foundation for writing clean, maintainable JavaScript code. While they originated in object-oriented programming, their concepts translate well to modern JavaScript development, whether you're building frontend applications, Node.js services, or full-stack solutions.
Remember that these are principles, not rigid rules. Use them as guidelines to improve your code quality, but always consider the context of your project and team. The goal is to write code that is easy to understand, maintain, and extend over time.
💡 Practice applying SOLID principles in your daily coding. Start small, refactor existing code gradually, and focus on one principle at a time until they become second nature.
Tags
Share this article
Enjoying the Content?
If this article helped you, consider buying me a coffee ☕
Your support helps me create more quality content for the community!
☕ Every coffee fuels more tutorials • 🚀 100% goes to creating better content • ❤️ Thank you for your support!
About Amr S.
Passionate about web development and sharing knowledge with the community. Writing about modern web technologies, best practices, and developer experiences.
More from Amr S.

Mastering Object-Oriented Programming in JavaScript: Complete Fundamentals Guide
Master JavaScript OOP fundamentals with this complete guide covering classes, inheritance, encapsulation, polymorphism, and best practices with practical examples.

Design Patterns in JavaScript: Essential Patterns for Modern Development
Learn key design patterns in JavaScript including Singleton, Factory, Observer, Strategy, and more with real-world examples and modern implementations.

Building Microservices with RabbitMQ: A Complete Guide
Learn how to implement message-driven microservices architecture using RabbitMQ with practical examples and best practices.