Principles, Not Rules
What SOLID is really for - designing for change - why the principles are heuristics not laws, and the messy ledger codebase we'll improve throughout the path.
On this page
Welcome to the Architecture path. The jump from engineer to senior engineer isn't more syntax - it's
judgment: knowing which design will still be workable in a year, and being able to defend the call. This
path teaches that judgment by improving real code. Our companion codebase, ledger-legacy, is a small but
genuinely messy accounting app - it works, it just hurts to change. Every module makes it a little better,
and every principle earns its place by fixing a concrete pain.
What SOLID is actually for
SOLID is five design principles, coined by Robert C. Martin, with one shared goal: code that's easy to change without fear. Not "correct" code - messy code can be correct. The enemy is code where a small change ripples unpredictably, where you can't add a feature without risking three others.
Read the principles through that lens and they stop being abstract:
- Single Responsibility - a class changes for one reason, so a change is contained.
- Open/Closed - add behavior without editing what already works.
- Liskov Substitution - subtypes behave, so polymorphism is safe.
- Interface Segregation - clients depend only on what they use.
- Dependency Inversion - policy depends on abstractions, so details can change.
Every one is really answering: when this code has to change, how much breaks?
Heuristics, not laws
Here's the senior mindset the whole path depends on: these are heuristics, not commandments. SOLID points you toward flexible designs, but flexibility has a cost - more classes, more indirection. Applied blindly it produces "enterprise" code so abstract nobody can follow it. Applied with judgment it produces code that bends where change actually happens and stays simple everywhere else.
So the real skill isn't reciting the principles - it's knowing when a violation is worth fixing. A 20-line script that violates every principle and never changes is fine. A core domain class touched weekly that violates SRP is a wound. Cost of change × likelihood of change is the dial.
Beware the abstraction tax
Every abstraction you add to satisfy a principle is code someone must read, understand, and navigate. Adding an interface 'in case we need another implementation' that never arrives is speculative generality - a real cost for an imagined benefit. Prefer to refactor toward flexibility when a second case actually appears (YAGNI), not before. SOLID earns its keep against real, recurring change - not hypothetical change.
Meet ledger-legacy
Throughout this path we refactor ledger-legacy: an app that records transactions, applies fees, and exports
reports. Its centerpiece is a 600-line LedgerManager class that reads files, parses formats, calculates fees
with a nest of ifs, writes to the database, and formats reports - all at once. It's the perfect patient:
everything is entangled, so every principle has an obvious target. We won't rewrite it; we'll evolve it,
which is how real systems improve.
Junior advice is 'this house is a mess - bulldoze it and start clean.' But the house is occupied and works: the plumbing runs, people live there. Senior work is renovation - you fix the load-bearing problems first, one room at a time, keeping the lights on throughout. SOLID is the building code you consult to decide which walls are safe to move and which are structural. You don't apply every regulation to a garden shed, but you absolutely apply them to the room everyone lives in.
Two classes violate the Single Responsibility Principle. Class A is a one-off migration script, run once last
year, never touched since. Class B is the core LedgerManager, edited in nearly every sprint, where a recent
fee change accidentally broke report formatting. Which do you refactor, and what principle guides the decision?
What is the core purpose of the SOLID principles?
Key takeaways
- SOLID's shared goal is code that's easy to change without fear - each principle limits how much breaks when a change is made.
- The five: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion - all answer 'when this changes, how much breaks?'
- They are heuristics, not laws: flexibility has a cost (more classes, indirection), so apply them where change actually happens.
- The dial is cost of change × likelihood of change; beware speculative abstraction (YAGNI) added for imagined future needs.
- This path teaches by refactoring the messy but working ledger-legacy codebase - evolving real code, not rewriting from scratch.