If you want to write cleaner, more maintainable, and scalable software, mastering the SOLID principles is essential. These five core object-oriented design principles empower you to build systems that are easier to understand, extend, and test — without introducing chaos.
Here's a quick breakdown:
S — Single Responsibility Principle (SRP)
Every class or module should have one clear responsibility.
When a class tries to do too many things, it becomes harder to maintain and more prone to bugs.
O — Open/Closed Principle (OCP)
Software should be open for extension, but closed for modification.
You should be able to add new features without rewriting existing code, minimizing the risk of regressions.
L — Liskov Substitution Principle (LSP)
Subtypes must be replaceable for their base types without altering the program’s behavior.
If a subclass breaks the expectations set by the parent class, it violates this principle.
I — Interface Segregation Principle (ISP)
Don't force a class to implement methods it doesn't need.
Instead of one large interface, create smaller, role-specific interfaces.
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules — both should depend on abstractions.
This decouples your code and makes it easier to modify and test.
The SOLID principles are foundational guidelines for object-oriented design. They help you create clean, maintainable, and scalable software systems. This guide includes detailed explanations and practical TypeScript examples to illustrate each principle.
S — Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have a single, well-defined responsibility.
Why It Matters: Mixing responsibilities (e.g., validation and persistence) makes a class fragile—changes to one task can break others. SRP improves modularity and testability.
Violation Example:
// SRP Violating Example interface Order { id: number; items: string[]; total: number; customerEmail: string; } class OrderProcessor { processOrder(order: Order): void { if (!order) throw new Error("Order cannot be null"); // 1. Validate order if (order.items.length === 0) { throw new Error("Order must have at least one item"); } // 2. Process payment console.log(`Processing payment of $${order.total} for order ${order.id}`); // 3. Save to database console.log(`Saving order ${order.id} with ${order.items.length} items to database`); // 4. Send confirmation email console.log(`Sending confirmation email to ${order.customerEmail}`); } }
This class handles too much, making it hard to maintain.
Refactored (SRP Applied):
interface Order { id: number; items: string[]; total: number; } class OrderValidator { validate(order: Order): void { if (!order) throw new Error("Order cannot be null"); } } class PaymentProcessor { processPayment(order: Order): void { console.log(`Processing payment for order ${order.id}`); } } class OrderRepository { save(order: Order): void { console.log(`Saving order ${order.id} to database`); } } class EmailNotifier { sendConfirmation(order: Order): void { console.log(`Sending confirmation email for order ${order.id}`); } } class OrderProcessor { private validator: OrderValidator; private paymentProcessor: PaymentProcessor; private repository: OrderRepository; private notifier: EmailNotifier; constructor( validator: OrderValidator, paymentProcessor: PaymentProcessor, repository: OrderRepository, notifier: EmailNotifier ) { this.validator = validator; this.paymentProcessor = paymentProcessor; this.repository = repository; this.notifier = notifier; } process(order: Order): void { this.validator.validate(order); this.paymentProcessor.processPayment(order); this.repository.save(order); this.notifier.sendConfirmation(order); } }
Each class now focuses on one task, enhancing clarity and flexibility.
O — Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
Why It Matters: Modifying existing code risks bugs. OCP allows new functionality via extensions (e.g., interfaces) without altering tested code.
Violation Example:
class InvoicePrinter { print(invoice: any, format: string): void { if (format === "PDF") { // Generate PDF } else if (format === "Excel") { // Generate Excel } } }
Adding a new format requires changing this class.
Refactored (OCP Applied):
interface Invoice { id: number; amount: number; } interface IInvoicePrinter { print(invoice: Invoice): void; } class PdfInvoicePrinter implements IInvoicePrinter { print(invoice: Invoice): void { console.log(`Printing PDF invoice for invoice ${invoice.id}`); } } class ExcelInvoicePrinter implements IInvoicePrinter { print(invoice: Invoice): void { console.log(`Printing Excel invoice for invoice ${invoice.id}`); } } class InvoiceService { private printer: IInvoicePrinter; constructor(printer: IInvoicePrinter) { if (!printer) throw new Error("Printer cannot be null"); this.printer = printer; } printInvoice(invoice: Invoice): void { this.printer.print(invoice); } }
New printers (e.g., HtmlInvoicePrinter
) can be added without modifying InvoiceService
.
L — Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without altering expected behavior.
Why It Matters: Violating LSP causes errors when subclasses don’t meet the parent’s contract.
Violation Example:
class Vehicle { startEngine(): void { // Start engine } } class ElectricCar extends Vehicle { startEngine(): void { throw new Error("Electric cars don’t have engines!"); } }
ElectricCar
breaks code expecting startEngine
.
Refactored (LSP Respected):
abstract class Vehicle { abstract start(): void; } interface ICombustionVehicle { startEngine(): void; } class GasolineCar extends Vehicle implements ICombustionVehicle { start(): void { this.startEngine(); } startEngine(): void { console.log("Gasoline engine started."); } } class ElectricCar extends Vehicle { start(): void { console.log("Electric motor activated."); } }
Both subclasses work as Vehicle
without issues.
I — Interface Segregation Principle (ISP)
Definition: Classes shouldn’t be forced to implement unused interfaces.
Why It Matters: Large interfaces create unnecessary dependencies. ISP favors smaller, specific interfaces.
Violation Example:
interface IWorker { work(): void; takeBreak(): void; } class Robot implements IWorker { work(): void { console.log("Robot working..."); } takeBreak(): void { throw new Error("Robots don’t take breaks!"); } }
Robot
is forced to implement an irrelevant method.
Refactored (ISP Applied):
interface IWorkable { work(): void; } interface IBreakable { takeBreak(): void; } class Human implements IWorkable, IBreakable { work(): void { console.log("Human working..."); } takeBreak(): void { console.log("Human taking a break..."); } } class Robot implements IWorkable { work(): void { console.log("Robot working..."); } }
Robot
only implements what it needs.
D — Dependency Inversion Principle (DIP)
Definition: High-level modules should depend on abstractions, not low-level details.
Why It Matters: Tight coupling limits flexibility. DIP uses abstractions for loose coupling.
Violation Example:
class Notification { private emailService = new EmailService(); send(message: string): void { this.emailService.sendEmail(message); } } class EmailService { sendEmail(message: string): void { console.log(`Email sent: ${message}`); } }
Notification
is tied to EmailService
.
Refactored (DIP Applied):
interface IMessageService { send(message: string): void; } class EmailService implements IMessageService { send(message: string): void { console.log(`Email sent: ${message}`); } } class SmsService implements IMessageService { send(message: string): void { console.log(`SMS sent: ${message}`); } } class Notification { private service: IMessageService; constructor(service: IMessageService) { if (!service) throw new Error("Service cannot be null"); this.service = service; } send(message: string): void { this.service.send(message); } }
Notification
works with any IMessageService
implementation.
Final Thoughts
Applying SOLID in TypeScript yields cleaner, more testable, and adaptable code.
Top comments (0)