Sitemap

SOLID Design Patterns

3 min readOct 22, 2026

--

SOLID is a set of five principles used in object-oriented programming to write clean, maintainable, and reusable code. These principles help developers build software that is easier to understand, test, and update.

Press enter or click to view image in full size
  1. S — Single Responsibility Principle

A class should have only one main responsibility. It should have only one main reason to change.

For example, calculating an invoice total and printing an invoice are two different responsibilities.

class Invoice {
constructor(public price: number) {}

calculateTotal(): number {
return this.price;
}
}

class InvoicePrinter {
print(invoice: Invoice): void {
console.log(`Total: ${invoice.calculateTotal()}`);
}
}

The Invoice class calculates the total, while the InvoicePrinter class prints it. Each class has a separate responsibility.

Real-world example: In an e-commerce application, separate order processing from sending email notifications.

2. O — Open/Closed Principle

Software should be open for extension but closed for modification. You should be able to add new functionality without unnecessarily changing existing code.

interface Discount {
calculate(price: number): number;
}

class NoDiscount implements Discount {
calculate(price: number): number {
return price;
}
}

class TenPercentDiscount implements Discount {
calculate(price: number): number {
return price * 0.9;
}
}

function checkout(price: number, discount: Discount): number {
return discount.calculate(price);
}

console.log(checkout(1000, new TenPercentDiscount()));
// 900

We can add a new discount class without changing the checkout() function.

Real-world example: Add a new discount type to an online shopping application without rewriting the checkout process.

3. L — Liskov Substitution Principle

Get Dahamakalanka’s stories in your inbox

Join Medium for free to get updates from this writer.

A child class should be usable wherever its parent class is expected, without breaking the program’s expected behavior.

abstract class Bird {
abstract move(): string;
}

class Sparrow extends Bird {
move(): string {
return "Flying";
}
}

class Penguin extends Bird {
move(): string {
return "Walking";
}
}

function moveBird(bird: Bird): void {
console.log(bird.move());
}

moveBird(new Sparrow()); // Flying
moveBird(new Penguin()); // Walking

Both Sparrow and Penguin can be used wherever a Bird is expected. Each follows the behavior defined by the parent class.

Real-world example: Different payment methods should work wherever the application expects a payment method, as long as they follow the same contract.

4. I — Interface Segregation Principle

A class should not be forced to implement methods it does not need. Instead of one large interface, create smaller interfaces for specific responsibilities.

interface Printer {
print(): void;
}

interface Scanner {
scan(): void;
}

class BasicPrinter implements Printer {
print(): void {
console.log("Printing document");
}
}

class MultiFunctionPrinter implements Printer, Scanner {
print(): void {
console.log("Printing document");
}

scan(): void {
console.log("Scanning document");
}
}

A basic printer only implements Printer. A multifunction printer implements both Printer and Scanner.

Real-world example: In a software application, separate read, write, and delete capabilities when different services need different operations.

5. D — Dependency Inversion Principle

High-level classes should depend on abstractions (interfaces), rather than directly depending on specific implementations.

interface PaymentGateway {
pay(amount: number): void;
}

class CardPayment implements PaymentGateway {
pay(amount: number): void {
console.log(`Paid ${amount} using card`);
}
}

class OrderService {
constructor(private payment: PaymentGateway) {}

checkout(amount: number): void {
this.payment.pay(amount);
}
}

const payment = new CardPayment();
const order = new OrderService(payment);

order.checkout(1000);

OrderService depends on the PaymentGateway interface, not directly on CardPayment. We can introduce another implementation, such as PayPalPayment, without rewriting the order service.

Real-world example: An application can change its payment provider while keeping most of its order-processing logic unchanged.

SOLID principles help you write code that is easier to maintain, extend, test, and reuse. They are design principles, rather than design patterns themselves.

--

--