Software Design Roadmap
Nine levels, each defined by what you can build and defend once you have it rather than by what you have read. The order matters: every level assumes the failure modes of the one before it, and the levels that look boring are the ones that decide whether the interesting ones work.
Names, functions and the first two forces: cohesion and coupling
You can write a module a colleague can read once and predict correctly — names that come from the business rather than from the framework, functions whose signature already tells you what they can and cannot do, and units that keep related things together instead of scattering one rule across a folder. More importantly you can look at an existing file and say which of its parts belong together and which are only neighbours, which is the observation everything later in this domain is built on.
Responsibilities, module interfaces and state you can see
You can take a feature request, decide who owns each piece of it, and give that owner an interface narrow enough that callers cannot reach past it — and you can model the thing's lifecycle as named states with named transitions instead of five booleans that admit combinations nobody has thought about. The concrete deliverable at this level is a module whose invariants cannot be broken from outside it, which is the first design property a test can actually prove.
Abstraction, dependency direction, pattern vocabulary and the domain model
You can build a feature whose business rules do not import the database, the HTTP framework or the payment vendor — the dependency arrows point one way, on purpose, and you can say out loud why. You can also name what you built using the pattern vocabulary without pretending the name was the design, and you can tell a real abstraction (one variation you have actually seen twice) from a speculative one you added because it felt tidy.
Refactoring under tests, reading smells, and letting review and testing talk back
You can change the shape of working code without changing what it does — in small steps, each of them shippable, each of them green — and you can name the smell that told you to start. You can also read a hard-to-write test as a design report rather than a chore: when a unit needs six mocks to stand up, you know that is the design telling you where the hidden dependencies are, and you know which move fixes it.
Package structure, architecture boundaries and the modular monolith
You can lay out a codebase so that a typical feature lands inside one directory instead of threading through four, and you can defend that layout against the change history rather than against a diagram. You can build a modular monolith whose modules have real contracts — enforced by the build, not by a convention nobody remembers — and you can tell an architecture boundary that earns its adapters from one that only adds a file to open.
Legacy code, safe migration and debt you can actually account for
You can change a decade-old service that has no tests and no author left, safely: observe the behaviour, characterize it, find a seam, make the change behind it, and prove nothing else moved. You can run a migration in expand-and-contract steps where old and new coexist and every step is independently revertible, and you can keep a debt register that a product manager will fund because each entry names an interest payment rather than a preference.
Evolvability, the complexity budget and decisions you write down
You can predict, before you build, which future changes this design makes cheap and which it makes expensive — and say the second half out loud, because a design that is claimed to make everything cheap has not been understood. You can build an extension point that a second team can use without your help, spend a complexity budget deliberately instead of accidentally, and leave a decision record that tells the engineer in two years what would make this choice wrong.
Large refactors, aggregates, and the repository as a design artefact
You can plan and land a refactor too big for one pull request: sequence it into slices that each ship, keep the old and new shapes coexisting while it runs, and know at every point how to stop without leaving the codebase worse. You can also draw consistency boundaries around the data a rule protects, argue about SOLID in terms of the change it makes cheap rather than the acronym, and lay out a repository so ownership and review actually match the way the code changes.
Designing a codebase that many teams change, and modernising one that already exists
You can design the parts of a codebase that outlive their authors: internal interfaces with a stated compatibility promise and a deprecation path, a feature-design template that forces states, failures and migration to be decided before code, and structures that keep one team's change from becoming another team's incident. You can also lead a modernisation — a strangler around a system nobody fully understands, delivered in increments that each pay for themselves — and know when the honest answer is that this system should be left alone.