Design, Architecture and System Design
Three different grains, three different reversibility profiles. Confusing them is why teams argue about folder layout as though it were a scaling decision.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Where does code design stop and architecture begin, and does the distinction actually matter?
A team is arguing about whether to split a module into two services. Half the discussion is about coupling and half is about scaling, and they are talking past each other.
It is all just design at different scales. The same principles apply, so the distinction is academic.
The principles rhyme; the *reversibility* does not, and that is the whole practical difference. Renaming a module is an afternoon. Splitting a service is a quarter, a migration and an on-call rota.
- The principles rhyme; the *reversibility* does not, and that is the whole practical difference. Renaming a module is an afternoon. Splitting a service is a quarter, a migration and an on-call rota.
- Treating them the same leads to the two characteristic mistakes: making a code-level decision with architecture-level ceremony, and making an architecture-level decision with code-level casualness.
- It also produces the argument in the requirement — one group optimising for change locality, the other for independent deployment, neither noticing they are answering different questions.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Both concerns are real; neither side is wrong about their own grain.
- The decision has to be made this week, and it is not equally reversible in both directions.
- Whatever grain is chosen, the invariants the code protects must still be protected — moving a boundary must not move a rule out of enforcement (Consistency Boundaries).
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Code design owns responsibilities, interfaces and dependencies inside a deployable unit.
- Architecture owns the boundaries between subsystems and the styles that govern them.
- System design owns the whole thing under load and failure — replication, partitioning, consistency.
- The boundary between the grains is reversibility, not size. A decision is architectural when undoing it requires coordinating other people or migrating data.
- That definition is more useful than any diagram, because it tells you how much evidence to demand before deciding.
Sorted by what it costs to undo
The usual definitions — "architecture is the important decisions", "architecture is the stuff that is hard to change" — are circular or unfalsifiable. Reversibility is neither, and it is directly actionable: it tells you how much evidence to gather before committing.
- The column that matters is the third one. Everything else follows from it.
- Anything requiring a data migration is at least architectural, whatever it looks like in the diff (Data Migration).
- Anything requiring another team to change is architectural regardless of how small the code change is — coordination is the cost.
| Grain | Example decision | Cost to undo | Evidence to demand |
|---|---|---|---|
| Code design | Extract a pricing module; rename a concept; introduce a value object | Hours to days, one person, no coordination | A named change it makes cheaper. Decide quickly, revisit freely. |
| Code design | Change an internal interface with several callers | Days, one team, mechanical | A short discussion. Cheap enough to try and see. |
| Architecture | Split a module into a separate service | A quarter, several teams, plus a migration and an on-call rota | A written decision with alternatives and a revisit trigger (Decision Records). |
| Architecture | Adopt an architectural style across the codebase | Months; every new file follows it | Strong evidence, and a pilot in one module first (Architecture Boundaries). |
| System design | Partition the database by tenant | Quarters, a data migration, and a period of dual writes | The most evidence you can get, plus a rollback plan you have tested. |
| System design | Choose a consistency model for a core workflow | Effectively permanent — every downstream feature assumes it | Prototype the failure cases, not the happy path. |
The argument this dissolves
The dispute in the requirement is a real one and it recurs everywhere: should this module become a service? The two sides are optimising different variables, and neither is being unreasonable.
What resolves it is noticing that a service split *does not* solve coupling. It relocates it, and converts a compile-time error into a runtime failure while adding a network, a deployment and a versioning problem. If the goal is change locality, the module boundary is the cheaper instrument. If the goal is independent deployment or independent scaling, the service is the right instrument and the coupling work still has to be done first.
Before: orders/ imports pricing/ in 14 places
After: orders-service --HTTP--> pricing-service
still 14 call sites
+ network latency on each
+ partial failure on each
+ a version skew problem
+ two deploys to change one rule
- the compiler no longer checks any of itStep 1: reduce 14 call sites to 1 interface
(this is the actual work, and it is the same
work either way)
Step 2: now ask the real question — do these two need
to deploy or scale independently?
If no: you are done, and it cost days.
If yes: the split is now mechanical, because
there is one seam instead of fourteen.The coupling work is required in both branches, and doing it first makes the split cheap and optional rather than urgent and risky. Splitting first means doing the same coupling work afterwards, across a network, without a compiler — which is strictly harder (What Changes at the Network Boundary).
How to build it
Most important first.
- Ask what it costs to undo. That single question sorts almost every decision into the right grain.
- Solve it at the smallest grain that can solve it. Most problems presented as architectural are module problems, and splitting a service to fix a coupling problem carries the coupling across the network (The Modular Monolith).
- When it genuinely is architectural, demand more evidence, write it down, and give it a revisit trigger (Decision Records).
- Keep the vocabulary straight in discussion. Half of these arguments dissolve when someone names the grain.
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Getting the grain right does not make any individual change cheaper. It changes how much you spend *deciding*, which is where large teams lose most of their time.
- The asymmetric case is the expensive one: an architectural decision made casually costs a quarter to undo, and that is the failure worth spending effort to avoid.
- The distinction adds vocabulary, and vocabulary has a learning cost. It pays off in argument time saved, which is invisible until you notice the arguments stopped.
- Reversibility is a spectrum, not a binary, so the line is fuzzy in the middle. It is still much better than no line.
What can go wrong
- Splitting services to solve a code-organisation problem, which converts a compile error into a runtime failure and a refactor into a migration (What Changes at the Network Boundary).
- Treating a schema change as a code decision, when it is the least reversible thing most teams do (Data Migration).
- Applying architecture ceremony — RFCs, review boards — to decisions that are an afternoon to undo, which teaches everyone to route around the process.
- Grains nest: a system-design decision constrains architecture, which constrains code design. The direction is one-way, and a code-level decision cannot fix a system-level mistake.
- That is why "we will fix it in the code" is not a valid answer to a boundary drawn in the wrong place.
- "Architecture is the important part; code design is detail." Most teams are slowed down by code-level problems, not architectural ones, and an excellent architecture full of god objects is not pleasant to work in (God Object).
- "Microservices are an architecture; monoliths are not." A monolith has an architecture; it is just internal (Designing a Monolith).
- "Architecture is decided up front." The reversible parts should be decided as late as possible; only the irreversible parts benefit from being decided early.
Testing it, and how it ages
- Each grain has a test boundary. Code design is tested by unit and module tests; architecture by contract tests between subsystems; system design by load and failure injection (Contract Tests).
- Testing an architectural boundary with unit tests is a category error, and it is how teams end up believing a split is safe when nothing has exercised the network path.
- Decisions migrate between grains over time. A module boundary that holds for two years may become a service boundary when a team needs independent deployment — and that is a promotion, not a correction.
- The reverse also happens and is under-used: services collapsing back into a modular monolith once the organisational reason for the split has gone.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALReversibility as the sorting criterion works regardless of stack, because it is about coordination and migration cost rather than about technology.
- SCALE-SPECIFICAt one team, nearly everything is reversible and the distinction barely matters; at fifty teams almost nothing is, because undoing anything requires coordinating people who did not make the decision. Advice about "architecture" from very large organisations is usually about coordination cost, not about code.
- CONTESTEDA defensible opposing view holds that the distinction is arbitrary and harmful — that it creates an "architect" role detached from code and licenses decisions made by people who will not live with them. That critique is largely right about the *role* and largely wrong about the *grain*: the reversibility difference is real whether or not anyone has the title.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — the third grain in full: replication, partitioning and consistency under load, where the decisions are least reversible of all.