SOLID Principles

How it works

SOLID Principles
How it works
  1. Input arrives
  2. Core component processes
  3. Result produced
  4. Observe and iterate

Learning Objectives

By the end of this topic, you will understand:

  • Apply Single Responsibility Principle to create focused, cohesive classes
  • Design extensible systems using Open/Closed Principle
  • Implement proper inheritance using Liskov Substitution Principle
  • Create minimal, focused interfaces following Interface Segregation
  • Invert dependencies to achieve flexible, testable architectures

Real-World Examples

Spring Boot: Dependency Inversion through IoC container, enabling flexible configuration and testing

Express.js Middleware: Open/Closed principle allows adding functionality without modifying core framework

TypeScript Interfaces: Interface Segregation in action with focused, role-based type definitions

Clean Architecture: All SOLID principles working together in hexagonal/onion architecture patterns

Overview

SOLID stands for Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
  • SRP: A class should have one reason to change.
  • OCP: Open for extension, closed for modification.
  • LSP: Subtypes must be substitutable for their base types.
  • ISP: Prefer many specific interfaces over a single general-purpose one.
  • DIP: Depend on abstractions, not concretions.

How it works

  1. Extract interfaces from concrete classes
  2. Create adapters for legacy code
  3. Use dependency injection to wire implementations
  4. Test via mocks/stubs targeting interfaces

Code example

Applying DIP and OCP with a pluggable Logger dependency injected into a service:
interface Logger { log(message: string): void }

class ConsoleLogger implements Logger {
  log(message: string): void { console.log('[LOG]', message) }
}

class FileLogger implements Logger {
  log(message: string): void { /* append to file */ }
}

class OrderService {
  constructor(private logger: Logger) {}
  placeOrder(orderId: string): void {
    // ... domain logic ...
    this.logger.log('Order placed: ' + orderId);
  }
}

// Open for extension (swap implementations), closed for modification (no code change in OrderService)
const svc = new OrderService(new ConsoleLogger());
svc.placeOrder('O-1001');

Implementation Notes

  • Use dependency injection frameworks to wire dependencies at runtime
  • Create abstract base classes or interfaces to define contracts
  • Apply Strategy pattern to implement Open/Closed principle effectively
  • Use composition root pattern to manage object creation and dependencies
  • Implement proper error handling that doesn't violate LSP

Best Practices

  • Start with concrete implementations, then extract abstractions as needed
  • Keep interfaces focused and role-based rather than feature-based
  • Use constructor injection for required dependencies
  • Design abstractions based on client needs, not implementation details
  • Apply SOLID principles gradually during refactoring sessions

Common Pitfalls

  • Over-engineering with unnecessary abstractions and interfaces
  • Violating LSP by throwing exceptions in derived classes
  • Creating fat interfaces that force clients to depend on unused methods
  • Mixing business logic with infrastructure concerns
  • Not considering the cost of indirection when applying DIP