Skip to content

SOLID Principles

intermediate2 min read
  • TypeScript

Five object-oriented design principles — single responsibility, open-closed, Liskov substitution, interface segregation, and dependency inversion — that keep code flexible and maintainable.

What you'll understand

  • What the five SOLID principles are
  • The common goal behind them
  • How they lead to flexible, maintainable code

Explanation

SOLID is a set of five object-oriented design principles that, followed together, make code easier to change without breaking. The letters stand for Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. They are guidelines, not laws, but they capture hard-won lessons about what keeps software maintainable as it grows.

Briefly: Single Responsibility says a class should have one reason to change, one job. Open-Closed says code should be open to extension but closed to modification, you add behaviour without rewriting what works. Liskov Substitution says a subtype must be usable anywhere its base type is expected. Interface Segregation says prefer small, focused interfaces over large catch-all ones. Dependency Inversion says depend on abstractions, not concrete implementations (the idea underneath dependency injection, see Related Topics).

The common thread is managing dependencies and change. Each principle reduces coupling, how tightly one piece is tied to another, so that a change in one place does not ripple everywhere. You do not apply them mechanically; you use them as lenses to notice when a design is becoming rigid or fragile. Overapplying them creates needless abstraction, so treat them as guidance toward flexible code, not boxes to tick.

Examples

Dependency Inversion in action: the service depends on an abstraction, not a concrete class, so implementations can change freely (see Related Topics):

interface Notifier {
  send(message: string): Promise<void>;
}

// Depends on the abstraction, not on EmailNotifier or SmsNotifier directly
class AlertService {
  constructor(private readonly notifier: Notifier) {}
}

Common mistakes

  • Giving one class many responsibilities, so every change touches it.
  • Modifying existing, working code to add a case instead of extending it.
  • Creating subtypes that cannot stand in for their base type.
  • Applying the principles so aggressively that the design drowns in abstraction.

Best practices

  • Give each class a single, clear responsibility.
  • Design so new behaviour is added by extension, not by editing what works.
  • Depend on small abstractions rather than concrete implementations (see Related Topics).
  • Use SOLID as guidance to reduce coupling, not as rules to over-apply.

Further reading

Related topics