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:
ifandswitchon 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:
newscattered 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 🟦
|
|
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:
- Model cohesive domain logic.
- Extend behavior through strategies or handlers instead of constant rewrites.
- Prefer composition and clear contracts over inheritance games.
- 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.