FoundationsGENERALSCALE-SPECIFICCONTESTED

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.

The question

Where does code design stop and architecture begin, and does the distinction actually matter?

The requirement

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.

The obvious build

It is all just design at different scales. The same principles apply, so the distinction is academic.

Why it breaks

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.

How it breaks as requirements change
  • 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.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

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.

Constraints
  • 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.
Invariants
  • 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.

Responsibilities
  • 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.
Boundaries
  • 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.
GrainExample decisionCost to undoEvidence to demand
Code designExtract a pricing module; rename a concept; introduce a value objectHours to days, one person, no coordinationA named change it makes cheaper. Decide quickly, revisit freely.
Code designChange an internal interface with several callersDays, one team, mechanicalA short discussion. Cheap enough to try and see.
ArchitectureSplit a module into a separate serviceA quarter, several teams, plus a migration and an on-call rotaA written decision with alternatives and a revisit trigger (Decision Records).
ArchitectureAdopt an architectural style across the codebaseMonths; every new file follows itStrong evidence, and a pilot in one module first (Architecture Boundaries).
System designPartition the database by tenantQuarters, a data migration, and a period of dual writesThe most evidence you can get, plus a rollback plan you have tested.
System designChoose a consistency model for a core workflowEffectively permanent — every downstream feature assumes itPrototype 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.

"This module is too coupled — let us make it a service"
Split 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 it
Fix the coupling, then decide
Step 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.

Cost of the next change
  • 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.
What the recommended approach costs
  • 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

Failure modes
  • 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.
Dependencies, and their direction
  • 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.
Misreads
  • "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

What to test, and at which boundary
  • 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.
How this design ages
  • 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.

Distributed Systemspartial-failure
Domains that do not exist yet
  • System Design — the third grain in full: replication, partitioning and consistency under load, where the decisions are least reversible of all.