Software Design Roadmap
Nine stages in one suggested order, starting with names, functions, cohesion and coupling — the file-level observations everything later is built on. Every stage names what it needs first and what you should be able to do before moving on. Progress is stored locally in your browser.
Where to start
Software engineering & design
9 stages · 0/196 lessonsHow to turn changing requirements into a codebase that stays understandable, testable and safe to change.
- Names, functions and the first two forces: cohesion and coupling
- Responsibilities, module interfaces and state you can see
- Abstraction, dependency direction, pattern vocabulary and the domain model
- Refactoring under tests, reading smells, and letting review and testing talk back
- Package structure, architecture boundaries and the modular monolith
- Legacy code, safe migration and debt you can actually account for
- Evolvability, the complexity budget and decisions you write down
- Large refactors, aggregates, and the repository as a design artefact
- Designing a codebase that many teams change, and modernising one that already exists
- 10/21
Names, functions and the first two forces: cohesion and coupling
Start hereThe domain starts at the file level, because every later structure is made of files someone has to read. What makes software hard to change and why the cost of the next change is the thing design optimises, names that come from the business rather than the framework, functions whose signature says what they can and cannot do, and the first two forces — cohesion and coupling — that every later stage measures itself against.
Before moving on: Look at an existing file and say which of its parts belong together and which are only neighbours, then rename and extract until a colleague can read it once and predict it correctly.
- What Makes Software Hard to Change
- The Cost of Change
- Changeability Is the Goal
- Local Reasoning
- Naming
- Naming and Domain Language
- Units in Names and Types
- Boolean Parameters
- Function Design
- Long Functions
- Extract Function
- Rename
- Comments
- Cohesion
- Kinds of Coupling
- Temporal Coupling
- Shared-State Coupling
- Duplicate Knowledge
- DRY: Knowledge, Not Lines
- KISS: Simplest for the Requirements You Have
- What a Code Smell Is
- 20/22
Responsibilities, module interfaces and state you can see
The design loop proper: from a requirement to its constraints, invariants and responsibilities, then to a module interface narrow enough that callers cannot reach past it. State sits here because a lifecycle written as named states and transitions is the first invariant a module can actually enforce, and error modeling closes the loop by treating failure as part of the contract rather than an afterthought.
Before moving on: Take a feature request, decide who owns each piece, give that owner an interface that protects its invariants, and model its lifecycle as states and transitions rather than a pile of booleans.
- The Design Loop
- Requirements Before Design
- Constraints Are Part of the Design
- Problem Decomposition
- Designing by Responsibility
- Single Responsibility, Carefully
- Separation of Concerns
- Encapsulation
- Information Hiding
- Designing a Module Interface
- Exposing Too Much
- Explicit State
- State Machines
- Invalid Transitions
- Boolean Flag Explosion
- State Ownership
- Optional Values and Absence
- Making Illegal States Unrepresentable
- Invariants
- Where Invariants Live
- Enforcing Invariants
- Error Modeling
- 30/22
Abstraction, dependency direction, pattern vocabulary and the domain model
With responsibilities settled, the question becomes which way the arrows point. What an abstraction costs and when duplication is cheaper, dependency direction, inversion and injection taught as three separate ideas, the pattern vocabulary as names for problems you have already met rather than designs to reach for, and the domain model — entities and value objects — as the place the business rules live.
Before moving on: Build a feature whose business rules do not import the database, the HTTP framework or the payment vendor, and explain why each dependency arrow points the way it does.
- What an Abstraction Actually Is
- What an Abstraction Costs
- The Rule of Three
- Leaky Abstractions
- Premature Abstraction
- Choosing the Model
- Dependency Direction
- Dependency Inversion
- Dependency Injection
- Constructor Injection
- Volatile Dependencies
- Wiring and the Composition Root
- Patterns as Vocabulary
- Strategy
- Factory
- Adapter
- Facade
- Pattern Overuse
- Domain Modeling
- Ubiquitous Language
- Entities
- Value Objects
- 40/22
Refactoring under tests, reading smells, and letting review and testing talk back
Now that you can say what a good structure looks like, learn to move working code towards it without changing what it does: the refactoring loop with a safety net, the named smells that tell you where to start and the case where each one is fine, and the two feedback channels — code review and hard-to-write tests — that report on the design before the next change prices it.
Before moving on: Refactor a module in small green steps, name the smell that prompted each one, and read a test that needs six mocks as a report on where the hidden dependencies are.
Needs first:Names, functions and the first two forces: cohesion and couplingAbstraction, dependency direction, pattern vocabulary and the domain model- What Refactoring Actually Is
- The Refactoring Loop
- Extract Module
- Move Responsibility
- Replace Conditional With Polymorphism
- Introduce Parameter Object
- God Object
- Shotgun Surgery
- Divergent Change
- Feature Envy
- Primitive Obsession
- Long Parameter List
- The Utility Dumping Ground
- What Code Review Is For
- A Review Checklist Worth Reading
- Review Size
- Tone, Disagreement and Receiving Review
- Review as Design Feedback — and Why It Arrives Too Late
- Testing as Design Feedback
- What a Unit Is
- Mocking
- Test Doubles, Precisely
- 50/22
Package structure, architecture boundaries and the modular monolith
Scale the dependency rules from one module to a whole codebase. Package by feature or by layer and what each does to change locality, cycles and fan-in / fan-out read off a real dependency graph, the layered, hexagonal, clean and onion styles compared without naming a winner, and the modular monolith whose module contracts are enforced by the build rather than by a convention nobody remembers.
Before moving on: Lay out a codebase so a typical feature lands in one directory, break a dependency cycle, and tell a boundary that earns its adapters from one that only adds a file to open.
Needs first:Abstraction, dependency direction, pattern vocabulary and the domain modelRefactoring under tests, reading smells, and letting review and testing talk back- Package Design
- Package by Layer
- Package by Feature
- Vertical Slices
- Module Granularity
- Circular Dependencies
- Breaking Cycles
- Stable Dependencies
- Dependency Cycles
- Fan-in and Fan-out
- Afferent and Efferent Coupling
- Architecture Boundaries
- Hexagonal Architecture (Ports and Adapters)
- Clean Architecture, and Where It Is Overused
- Onion Architecture
- Boundary Adapters
- Anti-Corruption Layer
- Designing a Monolith
- The Modular Monolith
- Internal Module Contracts
- Shared Libraries
- The Common Module
- 60/21
Legacy code, safe migration and debt you can actually account for
The refactoring discipline applied where it is hardest: code with no tests and no author left, and systems that must keep running while they change. Characterization tests and seams, the strangler and expand-and-contract, backward compatibility, versioned interfaces and data migration, and technical debt written down as an interest payment a product manager will fund rather than as a preference.
Before moving on: Change a decade-old service safely — observe, characterize, find a seam, change, verify — and run a migration in steps where old and new coexist and every step can be reverted.
- What "Legacy" Actually Means
- Characterization Tests
- Seams
- The Legacy Change Loop
- Refactoring Without Tests
- The Strangler Pattern
- The Risk in a Rewrite
- Incremental Migration
- Designing the Migration
- Backward Compatibility as a Constraint
- Versioned Interfaces
- Data Migration
- Expand and Contract
- Feature Flags and What They Cost
- What Technical Debt Actually Is
- Deliberate Debt
- Accidental Debt
- Interest: Why Debt Compounds
- The Debt Register
- Refactor or Rewrite
- Deprecation
- 70/22
Evolvability, the complexity budget and decisions you write down
Design judged over time rather than at review. Change amplification and encapsulation radius as the properties that decide whether the tenth change costs what the first did, extension points a second team can use without you, essential versus accidental complexity and the budget you spend on it, YAGNI and speculative generality, and decision records with revisit triggers so the reasoning outlives its author.
Before moving on: Predict which future changes a design makes cheap and which it makes expensive, say the second half out loud, and leave a decision record that names what would make the choice wrong.
Needs first:Package structure, architecture boundaries and the modular monolithLegacy code, safe migration and debt you can actually account for- Evolvability
- Change Amplification
- Encapsulation Radius
- Extensibility
- Plugin Architecture
- Stability and Dependency Direction
- Speculative Generality
- Essential and Accidental Complexity
- Simple Is Not Easy
- The Complexity Budget
- YAGNI, With Its Bill Attached
- Over-Design and Under-Design
- Over-Decomposition
- The Trade-off Matrix
- Decision Records
- Revisit Triggers
- Reversible and Irreversible Decisions
- Build, Library, SaaS or Managed Service
- Library or Framework
- What a Framework Charges
- Architecture Decision Records
- Docs Close to Code
- 80/22
Large refactors, aggregates, and the repository as a design artefact
Refactors too big for one pull request, sequenced into slices that each ship. Consistency boundaries, aggregates and domain services drawn around the data a rule protects — and the honest case for a transaction script when DDD does not pay. SOLID read critically, principle by principle, in terms of the change each one makes cheap, and the repository itself as a design artefact: ownership, review and bus factor matched to how the code actually changes.
Before moving on: Plan a multi-week refactor that can stop at any point without leaving the codebase worse, draw an aggregate around a real invariant, and argue for or against a SOLID principle by the change it makes cheap.
Needs first:Abstraction, dependency direction, pattern vocabulary and the domain modelLegacy code, safe migration and debt you can actually account forEvolvability, the complexity budget and decisions you write down- Design, Architecture and System Design
- Finding Seams
- Stable Boundaries
- Consistency Boundaries
- Invariant Leaks
- Aggregates
- The Aggregate Root
- Domain Services
- The Anemic Domain Model
- Transaction Script
- When Domain-Driven Design Does Not Pay
- SOLID, Read Honestly
- Single Responsibility, Critically
- Liskov Substitution, Critically
- Interface Segregation, Critically
- Dependency Inversion, Critically
- How SOLID Gets Misused
- Repository Structure
- Monorepo vs Polyrepo
- Code Ownership
- Bus Factor
- Design Review
- 90/22
Designing a codebase that many teams change, and modernising one that already exists
The last stage designs for people who are not in the room. 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, idempotency and partial failure chosen up front, debuggability as a property of the design, and a model treated as one more external dependency that is non-deterministic, fallible and costly.
Before moving on: Write a feature design that decides states, failures and migration before code, publish an internal interface with a versioning and deprecation policy, and say when a system should be left alone.
Needs first:Evolvability, the complexity budget and decisions you write downLarge refactors, aggregates, and the repository as a design artefact- When Design Does Not Pay
- Requirements Are a Snapshot
- The Requirements Nobody States
- Design for the Known, Name What You Assumed
- Dependency Management
- Do We Need a Package for This?
- API Stability
- Semantic Versioning
- RFCs
- Knowledge Sharing
- Designing a Feature Before Writing It
- A Feature Design Template
- Failure-Aware Feature Design
- Slicing a Feature
- Designing for Failure
- Idempotency by Design
- Partial Failure
- What Changes at the Network Boundary
- Debuggability by Design
- Designing a System That Has a Model In It
- The Model Is a Dependency
- Where the Probabilistic System Ends