Architecture documentation always drifts out of date. What concrete mechanisms keep it current, and what are their limits?
answer
- Docs in repo, reviewed in the same PR
- Generate structure; text diagrams that diff
- Rules become build failures (ArchUnit, fitness functions)
- PR trigger + CODEOWNERS + "last verified" date
- Rationale can't be generated; less doc = less staleness
basics
~20 sKeep docs in the repo next to the code and review them in the same pull requests (docs-as-code). Generate what can be generated — dependency and deployment diagrams from code and infrastructure files. Enforce structural rules with automated tests so violations fail the build, not the reader.
solid answer
~50 sDocumentation goes stale because updating it is optional while updating code is not. The fixes remove that asymmetry. **Docs-as-code**: Markdown/AsciiDoc in the repository, reviewed through the same pull requests, published by CI — so a structural change and its documentation land in one commit. **Generation over transcription**: derive module/dependency diagrams from the build graph or a tool like Structurizr, deployment topology from infrastructure-as-code, API docs from the spec; text-based diagrams (PlantUML, Mermaid, D2) diff in review. **Executable constraints**: architecture tests (ArchUnit-style layering and dependency rules) and **fitness functions** turn documented rules into build failures, so the doc cannot silently diverge. **Triggers and ownership**: a PR template or CODEOWNERS entry that forces a decision — update the doc, or write an ADR. **Ruthless subtraction**: the most reliable way to keep documentation current is to have less of it; delete anything with no named reader. Limits: rationale, quality scenarios, constraints and trade-offs can never be generated or tested — they need human writing and periodic review.
code
text · 10 linesCI pipeline (architecture-doc freshness)
1. generate module graph -> docs/views/modules.puml (from build graph)
2. generate deployment -> docs/views/deployment.puml (from IaC)
3. render *.puml/*.mmd -> static site
4. test layering + dependency rules (ArchUnit / dep-cruiser)
5. test fitness functions: no cycles, p99 budget, licence policy
6. check doc links, anchors, included code snippets compile
7. fail if any generated view differs from the committed one
// Not covered by any step above: rationale (ADRs), quality scenarios,
// constraints, risks -- hand-written, dated, reviewed each release.go deeper
Say docs live in the repo and change in the same pull request as the code, and that diagrams should be generated or written as text so they can be reviewed.
Add concrete tooling — PlantUML/Mermaid, generated module and deployment diagrams, PR checklists — and note which parts must stay hand-written.
Lead with the incentive asymmetry, then give the layered answer: generate facts, test rules with ArchUnit-style checks and fitness functions, write and date intent, delete the rest. Name the limits of each layer.
Treat it as an organisational operating model: what is mandatory versus optional across teams, how doc freshness is measured and made visible, how ADRs and fitness functions become governance rather than paperwork, and how to shrink the corpus deliberately.
## Why documentation rots — the real mechanism It is not laziness. It is an **incentive asymmetry**: if you don't change the code, the feature doesn't work and CI fails. If you don't change the document, nothing happens today — the cost lands months later on a different person. Every durable fix works by making documentation cheaper to keep right than to leave wrong, or by making "wrong" fail loudly. There is also a **surface-area problem**: staleness risk scales with volume. A 200-page description is guaranteed to be mostly wrong within a year; a 10-page one has a fighting chance. ## Mechanism 1 — docs-as-code Treat documentation as a build artefact of the same repository: - Plain-text source (**Markdown**, **AsciiDoc**) **in the code repository**, not a separate wiki. - Changed via **pull request**, reviewed by the same people who review the code, in the same diff. - **Published by CI** to a static site (so readers get a rendered version without leaving their browser). - Versioned with the code — the docs on a release branch describe that release. Why it works: it puts documentation inside the existing review ritual. A reviewer looking at a PR that adds a cross-module dependency sees the unchanged architecture doc in the same screen. The cost: wikis are easier for non-developers to edit. If product managers, ops, or compliance are genuine authors, you either teach them the PR flow or accept a split (structural docs in repo, business/process docs in the wiki) — and split ownership is itself a drift source. ## Mechanism 2 — generate, don't transcribe Anything that is a *fact about the code or infrastructure* should be **derived**, never hand-drawn: - **Module/dependency diagrams** from the build graph or module system (e.g. Spring Modulith emitting PlantUML, dependency-cruiser graphs for JS, `jdeps`, Gradle project graphs). - **Deployment topology** from infrastructure-as-code (Terraform/Helm/Compose) rather than a slide someone drew in 2023. - **API reference** from the OpenAPI/GraphQL/protobuf schema, ideally generated *from* the running code or contract tests. - **Structurizr / the C4 "model as code" approach**: define the model once in code or DSL, render Context/Container/Component diagrams from it, so all views share one source and cannot contradict each other. **Text-based diagrams** (PlantUML, Mermaid, D2, Graphviz) matter even when hand-written, because they diff. A binary `.png` or a Visio file cannot be reviewed; a 12-line Mermaid change can. Limit: generated diagrams show *what is*, never *what should be* or *why*. A generated dependency graph of a system with 400 edges is a hairball that documents nothing — generation still needs curation, filtering, and a legend. ## Mechanism 3 — executable constraints (the strongest lever) Convert documented rules into **tests that fail the build**: - **Architecture tests**: ArchUnit (JVM), dependency-cruiser (JS/TS), import-linter (Python), NetArchTest (.NET), plus module-system checks (Spring Modulith's `ModulithTest`, Java modules, Gradle module boundaries). "The domain layer must not import the web layer" stops being a sentence and becomes an assertion. - **Fitness functions** (Building Evolutionary Architectures, Ford/Parsons/Kua): automated checks over architectural characteristics — cyclic-dependency counts, coupling metrics, p99 latency budgets, licence policy, bundle-size ceilings. - **Contract tests** for interfaces between services, so the documented contract is the tested contract. - **Link/anchor checkers and snippet extraction** in CI: broken cross-references and code samples that no longer compile get caught. Including code into docs *by reference* (include a real file/region rather than pasting) means samples cannot drift. This is the only mechanism that produces the same failure pressure as code. Its limit is scope: you can test structure and measurable characteristics; you cannot test whether the *rationale* still holds. ## Mechanism 4 — triggers, ownership, review cadence - **PR template / checklist**: "Does this change a module boundary, an external dependency, or a quality trade-off? → update the affected section or add an ADR." - **CODEOWNERS** on the docs directory so the right reviewer is pulled in automatically. - **ADR log** for decisions, so the stable document doesn't churn — new reasoning appends rather than rewrites. - **A dated review cadence**: a quarterly or per-release pass over the small set of hand-written sections, with a "last verified" date visible in the rendered page. Staleness that is *visible* is far less dangerous than staleness that is silent. - **Doc debt in the backlog**: register known-stale sections as tickets rather than pretending the doc is fine. ## Mechanism 5 — write less The cheapest way to keep documentation current is to have less of it: - Delete any view or section for which you cannot name a reader and the decision they make with it. - Prefer **stable abstractions**: document the boundary and its contract, not the internals that change weekly. A doc pitched at the right altitude survives refactors. - Prefer **linking** to duplicating: one canonical place per fact. - "Minimum viable documentation": short, findable, correct beats comprehensive and wrong. A document that is 80% correct is worse than a one-pager that is 100% correct, because readers cannot tell which 20% to distrust. ## What cannot be automated Be honest about the residue: - **Rationale and rejected alternatives** — ADRs, written by humans. - **Quality attribute scenarios and their current measured values** — the scenario is human-written even if the measurement is automated. - **Constraints** imposed from outside (regulatory, contractual, organisational). - **Trade-offs, risks, technical debt** and the intent behind a boundary. - **The documentation roadmap** — which reader should read what. A mature setup is therefore a **hybrid**: generated structural views + tested constraints + a small hand-written core of intent, all in one repository, published by CI, with dates on the human-written parts. ## A useful framing for interviews > Facts about structure → **generate**. Rules about structure → **test**. Intent and reasoning → **write, date, and review**. Everything else → **delete**.
- Is a wiki ever the right home for architecture documentation?Yes, for the parts whose authors are not developers and whose lifecycle is not the code's: business context, stakeholder maps, process and onboarding material, meeting outcomes. What should not live there is anything that must change in lockstep with a commit — module structure, contracts, deployment topology, boundary rules — because a wiki has no mechanism to notice that the code moved. The failure mode of a split is ambiguity about which home is canonical, so state it explicitly and cross-link one way only.
- Your generated dependency diagram has 400 edges and is unreadable. Is generation still worth it?Generation is still worth it, but raw output is not documentation. Curate: generate per-boundary diagrams instead of one global graph, filter to the level of abstraction a reader can act on, collapse leaf detail, and add a legend and a short narrative. The generated artefact's real value in that state may be as a *test input* rather than a picture — assert "no cycles" and "module X must not reach module Y" in CI, and hand-draw a curated ten-box diagram for humans, with the test guaranteeing the hand-drawn one is not lying about the rules.
- How do you handle documentation that is only partly stale?Make the staleness visible rather than silent. Put a "last verified" date on every hand-written section, mark known-stale sections explicitly at the top with what is suspect, and file doc debt as backlog tickets. Readers can handle "this section was last checked in March and the deployment part is known out of date"; what destroys trust is a confident document that is quietly 20% wrong, because then nothing in it can be relied on. For structural content, prefer replacing the stale prose with a generated view so the problem cannot recur.
Keeping architecture docs current is like keeping a map of a construction site accurate. You survey the site automatically each night (generation), you install alarms on the fences so an unauthorised path trips immediately (architecture tests), and you keep a small hand-written note explaining why the fence is there at all — because no survey can ever discover intent.
saying these in an interview costs you the question
- Believing a documentation-review policy alone fixes drift; without a build-failing or generated mechanism the asymmetry remains.
- Storing architecture diagrams as binary images that cannot be diffed or reviewed in a pull request.
- Assuming everything can be generated — rationale, constraints, quality scenarios and risks are irreducibly human-written.
- Treating comprehensiveness as the goal; volume is the main driver of staleness.
- Keeping structural documentation in a wiki disconnected from the code lifecycle.
- Committing generated diagrams without a CI check that they still match the source, so the checked-in version silently diverges.
- Duplicating the same fact in several documents instead of one canonical place with links.