Every codebase starts clean. Six months later, one small change ripples through nine files, a bug fix in one feature breaks three others, and nobody wants to touch the checkout module because “that’s where the scary code lives.” That decay isn’t inevitable — it’s usually the result of code that was never designed to change. SOLID is a set of five design principles built to prevent exactly that.
The acronym covers Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Each one attacks a different kind of coupling, and together they form a practical checklist you can apply during design reviews, refactors, or that moment when a class file crosses 800 lines. The following sections break down each principle in plain language, show the warning signs that you’re violating it, and explain how to fix things without turning your project into an abstraction maze.
What SOLID Actually Solves
Before the definitions, get clear on the goal. SOLID isn’t about making code pretty or satisfying a linter. It’s about making code cheap to change. Every principle reduces the number of places you have to edit when requirements shift — and requirements always shift.
Think of it as blast radius management. Good design means a new feature touches a few files, a bug fix stays local, and adding a payment provider doesn’t require rewriting your order logic.
S — Single Responsibility Principle
A module should have one reason to change. That’s it. Note that “one reason to change” is about who asks for the change, not how many methods a class has. A class can have 20 methods and still have one responsibility.
The classic violation: an invoice class that calculates totals, formats a printable document, and emails the customer. Three separate stakeholders — accounting, design, and marketing — can each force a change to the same file. One edit can break all three features.
- Warning sign: your class name contains “And” or “Manager.”
- Warning sign: unrelated unit tests fail after a single-line change.
- Fix: split calculation, rendering, and delivery into separate collaborators that the invoice orchestrates.
This principle pairs naturally with clean architecture thinking: business rules should never be tangled with formatting or transport details.
O — Open/Closed Principle
Software entities should be open for extension but closed for modification. In practice: you should be able to add new behavior by writing new code, not by editing existing, working, tested code.
Picture a shipping cost calculator with a growing chain of if-else statements for each delivery type. Every new option means opening the same file and risking the existing branches. Refactor so each delivery type is its own implementation, registered in a lookup, and new options become new files with zero edits to the original logic.
The trap here is over-application. You cannot predict every future requirement, and premature extension points create dead abstractions nobody uses. Apply this principle after you’ve seen the same modification pattern twice — not before.
L — Liskov Substitution Principle
If a piece of code works with a base type, it must work with any subtype without surprises. Subtypes can’t strengthen preconditions, weaken postconditions, or throw exceptions the base type never promised.
The textbook example is a square inheriting from a rectangle: setting width independently breaks the square’s invariant, so code that treats squares as rectangles produces wrong area values. The lesson is broader than geometry — inheritance is a promise about behavior, not just a way to reuse fields.
- Warning sign: a subclass method throws “not supported” or returns empty as a stub.
- Warning sign: callers check the concrete type before calling a method.
- Fix: prefer composition over inheritance, and make sure subtypes honor the full contract.
I — Interface Segregation Principle
No client should be forced to depend on methods it doesn’t use. Fat interfaces force implementers to write stub methods and force callers to know about capabilities they’ll never touch — and every change to that interface hits everyone downstream.
Split broad contracts into small, role-specific ones. A reporting module that only needs to read data shouldn’t be forced to implement write and delete operations just to satisfy a single monolithic interface. Smaller interfaces also make test doubles trivial to write, which quietly speeds up your whole test suite.
D — Dependency Inversion Principle
High-level policy should not depend on low-level details. Both should depend on abstractions.
Concretely: your order-processing logic shouldn’t import a specific database driver or know how notifications are delivered. It should depend on a narrow contract — “save this order,” “notify this customer” — and let the infrastructure supply the implementation. Swap a database for an in-memory store during tests, or add a second notification channel, without touching business rules.
This is the principle that makes dependency injection frameworks worth the setup cost, and the one that most directly determines whether your codebase is testable at all.
How the Five Work Together
These principles aren’t independent rules — they reinforce each other in a chain:
- Single Responsibility keeps each unit small and focused.
- Open/Closed lets you extend those focused units without editing them.
- Liskov Substitution ensures the extensions actually behave correctly when swapped in.
- Interface Segregation keeps the contracts between units lean and honest.
- Dependency Inversion ensures the wiring happens at the edges, not inside your core logic.
Where SOLID Goes Wrong
SOLID is a set of heuristics, not commandments. Applied dogmatically, it produces interface explosions, six layers of indirection for a function that adds two numbers, and codebases where finding the actual implementation takes four jumps.
Use judgment. A 200-line script, a prototype, or a throwaway migration tool doesn’t need the full treatment. The principles pay off in code that lives for years, has multiple contributors, and changes often. Everywhere else, clarity beats ceremony.
A Practical Review Checklist
Next time you open a pull request, run these five questions past it:
- Does this class have more than one stakeholder who could request a change?
- Am I editing existing logic when I could be adding new code?
- Do all subtypes behave the way the base contract promises?
- Is any implementer writing methods it never uses?
- Does business logic know about specific infrastructure details?
Any “yes” isn’t a blocker — it’s a conversation starter. That’s the right way to use SOLID: as a shared vocabulary for design discussions, not as a gatekeeping tool.
Learn the five principles once and you’ll spot their absence everywhere — in bloated classes, duplicated feature logic, and tests that take minutes to run because everything is glued to a database. Apply them incrementally, refactor as you touch code, and the quality of your codebase will compound in your favor.
For more straight-talk breakdowns on developer tooling, architecture patterns, and the tech that actually matters, keep exploring TechBlazing — we’ll keep the signal high and the fluff out of the way.