Skip to main content

SOLID Principles

SOLID principles are design guidelines that help software teams build systems that are easy to extend, maintain, and understand. Think of them as architectural principles that prevent structural problems as applications grow in complexity.

What Are SOLID Principles?

SOLID is a mnemonic acronym introduced by Robert C. Martin (Uncle Bob) to describe five fundamental principles of object-oriented programming and design:

  • Single Responsibility Principle (SRP)
  • Open-Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

These principles work together to create software that is maintainable, extensible, and robust. They guide developers in creating systems that can evolve with changing requirements without requiring extensive rewrites.

Single Responsibility Principle (SRP)

🎯

Single Responsibility Principle

Technical

In business terms, SRP means assigning clear, distinct responsibilities to different parts of your system:

  • Similar to how organizations separate departments with specific responsibilities
  • Reduces risk when changes are needed, as changes affect smaller parts of the system
  • Improves team workflows since developers can work on isolated components
  • Makes onboarding easier as components have clear, focused purposes
🎯

Single Responsibility Principle

Non-Technical

A class should have only one reason to change, meaning it should have only one responsibility or job.

  • Focuses on cohesion - everything in the class should be related to a single purpose
  • Reduces code complexity and improves maintainability
  • Makes classes easier to understand, test, and modify
  • Enables better code organization and composition

SRP Example

Single Responsibility Principle

BeforeAvoid
class UserManager {
constructor(database) {
this.database = database;
}

// User data management
saveUser(user) {
// Validate user input
if (!user.email || !user.email.includes('@')) {
throw new Error('Invalid email');
}
if (!user.password || user.password.length < 8) {
throw new Error('Password too short');
}

// Hash password
const hashedPassword = this.hashPassword(user.password);
user.password = hashedPassword;

// Save to database
this.database.save('users', user);

// Send welcome email
this.sendWelcomeEmail(user.email);
}

hashPassword(password) {
// Complex password hashing logic
return 'hashed_' + password;
}

sendWelcomeEmail(email) {
// Email server configuration
const emailServer = {
host: 'smtp.example.com',
port: 587,
secure: true
};

// Email sending logic
console.log(`Email sent to ${email} via ${emailServer.host}`);
}
}
AfterRecommended
// User validation responsibility
class UserValidator {
validate(user) {
if (!user.email || !user.email.includes('@')) {
throw new Error('Invalid email');
}
if (!user.password || user.password.length < 8) {
throw new Error('Password too short');
}
return true;
}
}

// Password security responsibility
class PasswordService {
hashPassword(password) {
// Complex password hashing logic
return 'hashed_' + password;
}
}

// Email service responsibility
class EmailService {
constructor(emailConfig) {
this.emailConfig = emailConfig;
}

sendWelcomeEmail(email) {
// Email sending logic
console.log(`Email sent to ${email} via ${this.emailConfig.host}`);
}
}

// User management responsibility
class UserManager {
constructor(database, validator, passwordService, emailService) {
this.database = database;
this.validator = validator;
this.passwordService = passwordService;
this.emailService = emailService;
}

saveUser(user) {
// Validate
this.validator.validate(user);

// Hash password
user.password = this.passwordService.hashPassword(user.password);

// Save to database
this.database.save('users', user);

// Send welcome email
this.emailService.sendWelcomeEmail(user.email);
}
}

Open-Closed Principle (OCP)

🚪

Open-Closed Principle

Technical

From a business perspective, OCP enables safer evolution of your software:

  • Like how companies add new product lines without disrupting existing ones
  • Reduces regression risk when adding new features
  • Allows for more predictable release cycles and quality assurance
  • Enables incremental feature delivery without destabilizing the core system
🚪

Open-Closed Principle

Non-Technical

Software entities (classes, modules, functions) should be open for extension but closed for modification.

  • Adding new functionality should be done by adding new code, not changing existing code
  • Uses abstraction, inheritance, and composition to allow behavior extension
  • Reduces risk of introducing bugs in existing functionality
  • Promotes use of interfaces and abstract classes

OCP Example

Open-Closed Principle

BeforeAvoid
class PaymentProcessor {
processPayment(payment) {
if (payment.type === 'credit') {
// Process credit card payment
console.log(`Processing credit card payment of $${payment.amount}`);
// Connect to credit card gateway
// Validate credit card
// Charge the card
return true;
}
else if (payment.type === 'paypal') {
// Process PayPal payment
console.log(`Processing PayPal payment of $${payment.amount}`);
// Connect to PayPal API
// Redirect to PayPal
// Verify PayPal transaction
return true;
}
else if (payment.type === 'bitcoin') {
// Process Bitcoin payment
console.log(`Processing Bitcoin payment of $${payment.amount}`);
// Generate Bitcoin address
// Verify blockchain transaction
return true;
}

return false;
}
}
AfterRecommended
// Abstract payment method interface
class PaymentMethod {
processPayment(payment) {
throw new Error('Method not implemented');
}
}

// Concrete implementation for credit cards
class CreditCardPayment extends PaymentMethod {
processPayment(payment) {
console.log(`Processing credit card payment of $${payment.amount}`);
// Connect to credit card gateway
// Validate credit card
// Charge the card
return true;
}
}

// Concrete implementation for PayPal
class PayPalPayment extends PaymentMethod {
processPayment(payment) {
console.log(`Processing PayPal payment of $${payment.amount}`);
// Connect to PayPal API
// Redirect to PayPal
// Verify PayPal transaction
return true;
}
}

// Concrete implementation for Bitcoin
class BitcoinPayment extends PaymentMethod {
processPayment(payment) {
console.log(`Processing Bitcoin payment of $${payment.amount}`);
// Generate Bitcoin address
// Verify blockchain transaction
return true;
}
}

// Payment processor using the strategy pattern
class PaymentProcessor {
constructor() {
this.paymentMethods = {};
}

registerPaymentMethod(type, paymentMethod) {
this.paymentMethods[type] = paymentMethod;
}

processPayment(payment) {
if (this.paymentMethods[payment.type]) {
return this.paymentMethods[payment.type].processPayment(payment);
}
return false;
}
}

Liskov Substitution Principle (LSP)

🔄

Liskov Substitution Principle

Technical

In business terms, LSP ensures reliable substitution in your organization's systems:

  • Similar to how a manager expects any team member to fulfill their role competently
  • Prevents disruption when swapping components or service providers
  • Ensures that system upgrades and replacements work as expected
  • Provides confidence when extending functionality through inheritance
🔄

Liskov Substitution Principle

Non-Technical

Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

  • Subclasses must maintain the behavior expected from their base class
  • Ensures that inheritance hierarchies are modeled correctly
  • Constrains how contracts are implemented in inheritance hierarchies
  • Prevents unexpected behavior when using polymorphism

LSP Example

Liskov Substitution Principle

BeforeAvoid
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;
}
}

// Square is a special rectangle where width = height
class Square extends Rectangle {
constructor(size) {
super(size, size);
}

// Square must enforce width = height
setWidth(width) {
this.width = width;
this.height = width; // Side effect!
}

// Square must enforce width = height
setHeight(height) {
this.width = height; // Side effect!
this.height = height;
}
}

// Client code that uses a rectangle
function increaseRectangleWidth(rectangle) {
// Store the original height
const originalHeight = rectangle.height;

// Increase width by 2x
rectangle.setWidth(rectangle.width * 2);

// Verify the area calculation
console.log(`New area: ${rectangle.getArea()}`);

// Ensure original height is maintained
return rectangle.height === originalHeight;
}
AfterRecommended
// Base shape interface
class Shape {
getArea() {
throw new Error('Method not implemented');
}
}

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;
}
}

// Square is a separate shape, not a Rectangle subtype
class Square extends Shape {
constructor(size) {
super();
this.size = size;
}

setSize(size) {
this.size = size;
}

getArea() {
return this.size * this.size;
}
}

// Client code that uses shapes
function increaseShapeWidth(shape) {
if (shape instanceof Rectangle) {
const originalHeight = shape.height;
shape.setWidth(shape.width * 2);
return shape.height === originalHeight;
} else if (shape instanceof Square) {
// Handle square differently
const originalArea = shape.getArea();
shape.setSize(Math.sqrt(originalArea * 2));
return true;
}
}

Interface Segregation Principle (ISP)

✂️

Interface Segregation Principle

Technical

From a business perspective, ISP promotes focused, modular services:

  • Similar to offering customers specific service packages rather than all-or-nothing bundles
  • Reduces unnecessary dependencies between teams and systems
  • Enables more targeted updates and improvements
  • Allows for more flexible integration with external systems and partners
✂️

Interface Segregation Principle

Non-Technical

Clients should not be forced to depend on interfaces they do not use.

  • Prefers many specific interfaces over a single general-purpose interface
  • Reduces side effects and dependencies when interface changes
  • Makes systems more decoupled and easier to refactor, change, and redeploy
  • Prevents "fat interfaces" that force clients to implement unnecessary methods

ISP Example

Interface Segregation Principle

BeforeAvoid
// One large interface for all device operations
interface MultiFunctionDevice {
print(document);
scan(document);
fax(document);
copy(document);
staple(document);
bindDocument(document);
emailDocument(document, emailAddress);
authenticateUser(credentials);
}

// Implementation for a high-end office printer
class EnterpriseOfficePrinter implements MultiFunctionDevice {
print(document) {
console.log('Printing document...');
}

scan(document) {
console.log('Scanning document...');
}

fax(document) {
console.log('Faxing document...');
}

copy(document) {
console.log('Copying document...');
}

staple(document) {
console.log('Stapling document...');
}

bindDocument(document) {
console.log('Binding document...');
}

emailDocument(document, emailAddress) {
console.log(`Emailing document to ${emailAddress}...`);
}

authenticateUser(credentials) {
console.log('Authenticating user...');
return true;
}
}

// Implementation for a basic home printer
// Must implement ALL methods, even unsupported ones
class BasicHomePrinter implements MultiFunctionDevice {
print(document) {
console.log('Printing document...');
}

scan(document) {
console.log('Scanning document...');
}

// Methods that are not supported must still be implemented
// with empty or throw implementations
fax(document) {
throw new Error('Fax not supported on this device');
}

copy(document) {
console.log('Copying document...');
}

staple(document) {
throw new Error('Stapling not supported on this device');
}

bindDocument(document) {
throw new Error('Binding not supported on this device');
}

emailDocument(document, emailAddress) {
throw new Error('Email not supported on this device');
}

authenticateUser(credentials) {
throw new Error('Authentication not supported on this device');
}
}
AfterRecommended
// Segregated interfaces for different device capabilities
interface Printer {
print(document);
}

interface Scanner {
scan(document);
}

interface Copier {
copy(document);
}

interface Fax {
fax(document);
}

interface DocumentFinisher {
staple(document);
bindDocument(document);
}

interface NetworkEnabled {
emailDocument(document, emailAddress);
authenticateUser(credentials);
}

// Implementation for a high-end office printer
// Only implements the interfaces it supports
class EnterpriseOfficePrinter implements
Printer, Scanner, Copier, Fax, DocumentFinisher, NetworkEnabled {

print(document) {
console.log('Printing document...');
}

scan(document) {
console.log('Scanning document...');
}

fax(document) {
console.log('Faxing document...');
}

copy(document) {
console.log('Copying document...');
}

staple(document) {
console.log('Stapling document...');
}

bindDocument(document) {
console.log('Binding document...');
}

emailDocument(document, emailAddress) {
console.log(`Emailing document to ${emailAddress}...`);
}

authenticateUser(credentials) {
console.log('Authenticating user...');
return true;
}
}

// Implementation for a basic home printer
// Only implements the interfaces it actually supports
class BasicHomePrinter implements Printer, Scanner, Copier {
print(document) {
console.log('Printing document...');
}

scan(document) {
console.log('Scanning document...');
}

copy(document) {
console.log('Copying document...');
}

// No need to implement unsupported methods
}

Dependency Inversion Principle (DIP)

⚖️

Dependency Inversion Principle

Technical

In business terms, DIP establishes flexible, interchangeable components:

  • Similar to how businesses define standard interfaces for suppliers to implement
  • Reduces vendor lock-in and allows changing implementations without disruption
  • Simplifies testing and quality assurance processes
  • Enables gradual system modernization by swapping components incrementally
⚖️

Dependency Inversion Principle

Non-Technical

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

  • Decouples software modules through abstractions
  • Uses dependency injection to provide implementation at runtime
  • Makes code more testable through mock implementations
  • Enables flexibility to swap implementations without changing clients

DIP Example

Dependency Inversion Principle

BeforeAvoid
// Low-level module
class MySQLDatabase {
constructor() {
// MySQL specific connection setup
this.connection = 'MySQL Connection';
console.log('MySQL Database connected');
}

save(data) {
console.log(`Saving data to MySQL database: ${JSON.stringify(data)}`);
}

delete(id) {
console.log(`Deleting record ${id} from MySQL database`);
}

update(id, data) {
console.log(`Updating record ${id} in MySQL database`);
}

find(id) {
console.log(`Finding record ${id} in MySQL database`);
return { id, name: 'Test Record' };
}
}

// High-level module that directly depends on the low-level module
class UserService {
constructor() {
// Direct dependency on MySQL implementation
this.database = new MySQLDatabase();
}

createUser(user) {
// Some business logic for validation
if (!user.name || !user.email) {
throw new Error('User must have name and email');
}

// Save to MySQL
this.database.save(user);
return true;
}

deleteUser(userId) {
this.database.delete(userId);
return true;
}

updateUser(userId, userData) {
// Some business logic
if (userData.role === 'admin' && !userData.approvedBy) {
throw new Error('Admin users must be approved');
}

this.database.update(userId, userData);
return true;
}

findUser(userId) {
return this.database.find(userId);
}
}
AfterRecommended
// Abstract database interface (abstraction)
class DatabaseInterface {
save(data) {
throw new Error('Method not implemented');
}

delete(id) {
throw new Error('Method not implemented');
}

update(id, data) {
throw new Error('Method not implemented');
}

find(id) {
throw new Error('Method not implemented');
}
}

// Low-level module implementing the interface
class MySQLDatabase extends DatabaseInterface {
constructor() {
super();
// MySQL specific connection setup
this.connection = 'MySQL Connection';
console.log('MySQL Database connected');
}

save(data) {
console.log(`Saving data to MySQL database: ${JSON.stringify(data)}`);
}

delete(id) {
console.log(`Deleting record ${id} from MySQL database`);
}

update(id, data) {
console.log(`Updating record ${id} in MySQL database`);
}

find(id) {
console.log(`Finding record ${id} in MySQL database`);
return { id, name: 'Test Record' };
}
}

// Another implementation of the same interface
class MongoDBDatabase extends DatabaseInterface {
constructor() {
super();
// MongoDB specific connection setup
this.connection = 'MongoDB Connection';
console.log('MongoDB Database connected');
}

save(data) {
console.log(`Saving document to MongoDB: ${JSON.stringify(data)}`);
}

delete(id) {
console.log(`Deleting document ${id} from MongoDB`);
}

update(id, data) {
console.log(`Updating document ${id} in MongoDB`);
}

find(id) {
console.log(`Finding document ${id} in MongoDB`);
return { id, name: 'Test Record' };
}
}

// High-level module that depends on abstraction
class UserService {
// Dependency injection through constructor
constructor(database) {
// Depends on abstraction, not concrete implementation
this.database = database;
}

createUser(user) {
// Business logic remains the same
if (!user.name || !user.email) {
throw new Error('User must have name and email');
}

// Save using the injected database
this.database.save(user);
return true;
}

deleteUser(userId) {
this.database.delete(userId);
return true;
}

updateUser(userId, userData) {
// Business logic remains the same
if (userData.role === 'admin' && !userData.approvedBy) {
throw new Error('Admin users must be approved');
}

this.database.update(userId, userData);
return true;
}

findUser(userId) {
return this.database.find(userId);
}
}

SOLID Principles in Practice

SOLID Principles Working Together

How the five SOLID principles interact to create more maintainable software

100%
🔍 Use Ctrl+Scroll to zoom
CreatesEnablesIncreasesReducesEnhancesImprovesImprovesReducesMinimizesEnablesSingleResponsibilityPrincipleOpen-ClosedPrincipleLiskovSubstitutionPrincipleInterfaceSegregationPrincipleDependencyInversionPrincipleHighCohesionLowCouplingImprovedTestabilityDesignFlexibility

Legend

Components
Principle
Outcome
Connection Types
Process Flow
Creates
Enables
Increases

When to Apply SOLID Principles

SOLID principles are most valuable in the following scenarios:

  1. Medium to large applications that will be maintained over time
  2. Team environments where multiple developers work on the same codebase
  3. Software that will evolve with changing requirements
  4. Systems requiring high testability for quality assurance
  5. Reusable libraries or frameworks that will be used by other developers

These principles may add complexity to very small projects or prototypes where simplicity and rapid development are more important than long-term maintainability.

Real-World Benefits

💼

Business Benefits of SOLID

Technical

Business outcomes from SOLID-based development:

  • Lower maintenance costs over the application lifecycle
  • Faster time-to-market for new features and enhancements
  • Reduced technical debt and associated business risks
  • Improved onboarding experience for new team members
  • Higher customer satisfaction through more reliable software
💼

Business Benefits of SOLID

Non-Technical

Technical benefits of applying SOLID principles:

  • Reduced bug density in complex systems
  • Easier isolation and fixing of defects when they occur
  • Improved testability through smaller, focused components
  • More efficient team collaboration on the same codebase
  • Better adaptability to changing requirements and technologies

SOLID Principles in Different Programming Languages

SOLID principles are language-agnostic and can be applied in any object-oriented programming language. However, the implementation details may vary:

Java and C#

  • Rich support for interfaces and abstract classes makes implementing ISP and DIP straightforward
  • Strong typing helps enforce LSP contracts
  • Established dependency injection frameworks like Spring and .NET Core support DIP

JavaScript and TypeScript

  • Prototypal inheritance and functional composition provide alternatives to traditional OOP
  • TypeScript's interfaces and abstract classes support SOLID directly
  • JavaScript can apply SOLID through patterns and conventions
  • Modern frameworks like Angular embrace dependency injection and SOLID

Python and Ruby

  • Duck typing requires more discipline to ensure LSP compliance
  • Dynamic nature allows for flexible composition patterns
  • Metaprogramming capabilities offer unique ways to implement OCP
  • Interfaces are conventional rather than enforced by the language

Common Anti-Patterns and How to Fix Them

God Class Anti-Pattern (Violates SRP)

Problem: A class that knows or does too much, often with hundreds or thousands of lines of code.

Solution:

  1. Identify distinct responsibilities
  2. Extract each responsibility into its own class
  3. Use composition to reconstruct the original functionality

Rigidity Anti-Pattern (Violates OCP)

Problem: Systems where one change affects many parts of the codebase.

Solution:

  1. Identify points of frequent change
  2. Introduce abstractions at these points
  3. Make concrete implementations depend on these abstractions

Fragile Base Class Anti-Pattern (Violates LSP)

Problem: Base class changes break derived classes in unexpected ways.

Solution:

  1. Make base classes more abstract with fewer implementation details
  2. Follow "composition over inheritance" when appropriate
  3. Document and enforce class invariants and contracts

Interface Bloat Anti-Pattern (Violates ISP)

Problem: Large interfaces forcing clients to implement methods they don't need.

Solution:

  1. Break large interfaces into smaller, more cohesive ones
  2. Use composition of interfaces for classes needing multiple capabilities
  3. Apply the Interface Adapter pattern for backward compatibility

Concrete Dependency Anti-Pattern (Violates DIP)

Problem: High-level modules directly instantiate and depend on low-level modules.

Solution:

  1. Introduce abstractions that both modules depend on
  2. Use dependency injection to provide concrete implementations
  3. Use factories or dependency injection containers to manage object creation

Additional Resources