Skip to content
S.O.L.I.D.

SOLID: principles for code that ages well 🧱

Why SOLID? 🤔

As a project grows, code tends to become an apartment building with exposed wires, noisy pipes, and neighbors arguing about dependencies. SOLID principles are the rules of good neighborhood behavior: no magic, just practices that make software easier to extend, test, and maintain.

In short: less coupling, more cohesion, healthier dependencies. The result is less painful refactoring and features that enter without demolishing half the building.


SRP: single responsibility 🧹

Single Responsibility does not mean “does only one tiny thing”. It means a module should have one reason to change.

  • In practice: separate orchestration, domain logic, and I/O.
  • Warning signs: methods with mixed intent, cascading dependencies, tests that must instantiate half the city.
  • Useful refactoring: extract dedicated services, prefer composition, isolate I/O with adapters.

OCP: open/closed 🚪

Open for extension, closed for modification. New behavior should often arrive by adding something, not by rewriting stable code.

  • In practice: work through abstractions, strategies, handlers, or plugins.
  • Warning signs: if and switch on type spread everywhere.
  • Useful refactoring: Strategy, Factory, Template Method, registries, declarative configuration.

LSP: Liskov substitution 🔁

Subtypes should be replaceable without surprising the caller. If the child breaks the expectations of the parent contract, the model is probably lying.

  • In practice: preserve invariants and semantics.
  • Warning signs: “not supported” overrides, stricter preconditions, weaker guarantees.
  • Useful refactoring: revise the hierarchy, prefer composition over inheritance, split interfaces.

ISP: interface segregation ✂️

Clients should not depend on methods they do not use.

  • In practice: smaller, coherent interfaces; separate commands from queries where it helps.
  • Warning signs: no-op methods, mocks with absurd setup, types that implement behaviors they do not actually need.
  • Useful refactoring: capability-based interfaces, adapters, thinner contracts.

DIP: dependency inversion 🔌

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

  • In practice: define contracts close to the domain; keep framework, DB, and SDK details at the edges.
  • Warning signs: new scattered everywhere, business logic importing infrastructure code, impossible unit tests.
  • Useful refactoring: ports and adapters, constructor injection, factories for expensive resources, inversion of control for orchestration.

A compact example in TypeScript 🟦

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
type User = { email: string };

class UserValidator {
 isValid(u: User): boolean {
  return /\S+@\S+\.\S+/.test(u.email);
 }
}

interface UserRepo {
 save(u: User): Promise<void>;
}

class PostgresUserRepo implements UserRepo {
 async save(u: User) {
 }
}

interface Mailer {
 send(to: string, subject: string, body: string): Promise<void>;
}

class SmtpMailer implements Mailer {
 async send(to: string, subject: string, body: string) {
 }
}

class UserService {
 constructor(
  private validator: UserValidator,
  private repo: UserRepo,
  private mailer: Mailer,
 ) {}

 async register(u: User) {
  if (!this.validator.isValid(u)) throw new Error('Invalid email');
  await this.repo.save(u);
  await this.mailer.send(u.email, 'Welcome', 'Thanks for registering!');
 }
}

This small example already shows SRP, DIP, and a healthier dependency structure. No sermons needed.


Putting them together in the real world 🧩

Healthy code often follows a flow like this:

  1. Model cohesive domain logic.
  2. Extend behavior through strategies or handlers instead of constant rewrites.
  3. Prefer composition and clear contracts over inheritance games.
  4. Push technical details to the edges and depend on interfaces where the volatility actually is.

SOLID is not a religious checklist. It is a set of heuristics to manage change. Use it where it reduces friction. Drop the dogma where it only produces decorative abstractions.

Last updated on