Architecture Diagrams That Stay Accurate: Generated vs Curated

Hand-drawn architecture diagrams go stale within months. Here is why it happens and a practical system for keeping diagrams honest: curate intent, generate structure, and check one against the other.

By The OpenVisio Team · · 13 min read

Every team has the diagram. It lives in a slide deck or a Confluence page, someone drew it during a planning offsite two years ago, and it shows five neat boxes connected by tidy arrows. New hires are sent to it on day one. Then they open the repository and discover a sixth service nobody drew, a queue that replaced one of the arrows, and a "shared" library that half the boxes now import directly. The diagram was accurate once. It is not accurate now, and nobody can say exactly when it stopped being true.

This post explains why architecture diagrams rot, and lays out a practical system for keeping them honest. The short version: split diagrams into the parts that express intent (which humans should curate) and the parts that describe structure (which machines should generate), then check the two against each other automatically.

Key takeaways

  • Diagrams rot because they are disconnected from the code: nothing fails when they become wrong, so they become wrong silently.
  • Separate intent (what the system is supposed to look like, and why) from structure (what the code actually imports and calls). Curate the first, generate the second.
  • Store curated diagrams as text in the repository (Mermaid, PlantUML, D2, Structurizr DSL) so they are reviewed in pull requests like code.
  • Generate structural views from the code on demand or in CI with tools such as dependency-cruiser, madge, pydeps or go list.
  • Add a fitness check that compares the curated diagram's allowed dependencies with the real ones and fails the build when they diverge.
  • Put a date and an owner on every curated diagram, and delete diagrams nobody will maintain.

Why diagrams go stale

A diagram is a claim about the code. Code changes many times a day; a diagram changes when someone remembers to update it. The gap between those two rates is the whole problem, and it has a few specific causes.

No feedback loop. When you rename a function, the compiler complains about every caller. When you add a dependency from the billing module to the notifications module, nothing complains about the diagram that says those modules are independent. Errors that produce no signal accumulate.

Wrong storage. Diagrams drawn in a whiteboard tool or a slide are binary blobs, stored away from the code, edited by whoever has the login. They are not part of the pull request that changes the architecture, so the person making the change never sees them.

Mixed abstraction levels. Many diagrams try to show everything: the services, the databases, a few important classes, the deployment topology. A diagram at five levels of detail has five times as many ways to go wrong, and the detailed parts (class names, specific endpoints) go stale fastest.

Unclear ownership. If a diagram belongs to "the team", it belongs to nobody. The person who drew it moves on, and everyone else assumes it is someone else's job.

None of these are fixed by drawing better diagrams. They are fixed by changing where diagrams live, what they contain, and what checks them.

Curated versus generated: two different jobs

It helps to stop thinking of "the architecture diagram" as one thing. There are two jobs, and they need different tools.

Curated diagramsGenerated diagrams
AnswersWhat should the system look like, and why?What does the code actually do right now?
Source of truthHuman judgment and decisionsImports, calls, config, manifests
Good atIntent, context, naming, emphasis, omissionCompleteness, accuracy, finding surprises
Bad atStaying currentExplaining why; choosing what matters
Changes whenA decision changesAny commit changes structure
Example toolsStructurizr DSL, Mermaid, PlantUML, D2dependency-cruiser, madge, pydeps, go list, jdeps

A curated diagram is valuable precisely because it leaves things out. It shows the four containers that matter to a product manager and hides the 40 internal packages. A generated diagram is valuable precisely because it leaves nothing out. It shows the import from billing/ into notifications/internal/ that nobody intended.

The mistake is asking one diagram to do both jobs. Hand-drawn diagrams that try to be complete rot. Generated diagrams that try to explain intent become unreadable hairballs. Keep them separate, and let each do what it is good at. The C4 model vs generated maps post goes deeper into how to layer the two.

Step 1: Move curated diagrams into the repository as text

The cheapest improvement is to store diagrams as text files next to the code they describe. Text diagrams show up in diffs, get reviewed in pull requests, and can be searched with grep.

Mermaid is rendered natively by GitHub and GitLab, which makes it the lowest-friction choice for many teams:

flowchart LR
  web[Web app<br/>Next.js] -->|HTTPS/JSON| api[API<br/>Go]
  api -->|SQL| db[(PostgreSQL)]
  api -->|publish| queue[[Order events]]
  queue -->|consume| worker[Fulfillment worker]

For larger systems, Structurizr DSL lets you define the model once and render several views from it, which avoids the classic problem of three diagrams disagreeing about the same box. D2 and PlantUML are reasonable alternatives with better layout control.

A few conventions make text diagrams much more durable:

  • Put each diagram in a docs/architecture/ folder, or next to the module it describes (services/billing/ARCHITECTURE.md).
  • Add a header with an owner and a last-reviewed date. A diagram with "Last reviewed: 2024-03" is honest about its age; one with no date is not.
  • Use the same names as the code. If the folder is fulfillment-worker, the box is fulfillment-worker, not "Order Processing Engine".
  • Add the diagram's path to CODEOWNERS so the owning team is asked to review changes near it.

Step 2: Generate structural views from the code

For the structural truth, generate. Most ecosystems have a tool that turns imports into a graph:

# JavaScript / TypeScript: dependency-cruiser
npx depcruise src --include-only "^src" --output-type dot | dot -T svg > deps.svg

# JavaScript / TypeScript: madge
npx madge --extensions ts,tsx --ts-config tsconfig.json --image deps.svg src/

# Python: pydeps
pydeps mypackage --max-bacon 2 --cluster -o deps.svg

# Go: list each package and its imports
go list -f '{{ .ImportPath }} -> {{ join .Imports " " }}' ./...

# Java: jdeps summary of package dependencies
jdeps -summary -recursive target/classes

Two rules keep generated diagrams useful instead of overwhelming.

Collapse to the level you care about. A file-level graph of a 3,000-file repository is noise. dependency-cruiser's --collapse option (for example --collapse "^src/[^/]+") folds files into their top-level folders, which usually matches how people think about modules. pydeps has --max-bacon to limit depth; madge can be pointed at a single subfolder.

Generate on demand, not once. A generated SVG committed to the repo is just a hand-drawn diagram with extra steps: it rots the moment someone forgets to regenerate it. Either generate in CI and publish the artifact, or use a tool that builds the view live from the current code.

For reading the output, the dependency graph explained post covers the common shapes (hubs, layers, cycles, stars) and what each usually means.

Step 3: Check intent against structure in CI

This is the step that actually stops rot. Once the curated diagram expresses intent ("the web app talks to the API, the API talks to the database, nothing else talks to the database"), you can turn that intent into rules and test them against the generated graph on every pull request.

With dependency-cruiser, rules live in .dependency-cruiser.js:

module.exports = {
  forbidden: [
    {
      name: 'web-not-to-db',
      comment: 'The web app goes through the API. See docs/architecture/containers.md',
      severity: 'error',
      from: { path: '^src/web' },
      to: { path: '^src/db' },
    },
    {
      name: 'no-circular',
      severity: 'error',
      from: {},
      to: { circular: true },
    },
  ],
}
npx depcruise src --config .dependency-cruiser.js

In Python, import-linter does the same with contracts in pyproject.toml or .importlinter; in Java, ArchUnit expresses the rules as unit tests; in an Nx monorepo, the @nx/enforce-module-boundaries lint rule enforces tag-based constraints. The module boundaries and coupling post has full examples for each.

Note the comment field linking back to the curated diagram. When the rule fails, the developer is pointed at the diagram, sees the intent, and either fixes the code or opens a pull request that updates both the rule and the diagram. That is the feedback loop that was missing.

A lightweight drift report

If you are not ready for hard rules, start with a report. A short script can extract the edges from your curated Mermaid file and compare them to the generated graph:

// scripts/diagram-drift.mjs
import { readFileSync } from 'node:fs'
import { execSync } from 'node:child_process'

const mermaid = readFileSync('docs/architecture/modules.md', 'utf8')
const declared = new Set(
  [...mermaid.matchAll(/(\w+)\s*-->(?:\|[^|]*\|)?\s*(\w+)/g)].map((m) => `${m[1]}->${m[2]}`),
)

const json = JSON.parse(
  execSync('npx depcruise src --collapse "^src/[^/]+" --output-type json', { encoding: 'utf8' }),
)
const actual = new Set()
for (const mod of json.modules) {
  const from = mod.source.split('/')[1]
  for (const dep of mod.dependencies) {
    const to = dep.resolved.split('/')[1]
    if (dep.resolved.startsWith('src/') && from !== to) actual.add(`${from}->${to}`)
  }
}

const undeclared = [...actual].filter((e) => !declared.has(e))
const missing = [...declared].filter((e) => !actual.has(e))
console.log('In code but not in diagram:', undeclared)
console.log('In diagram but not in code:', missing)
process.exitCode = undeclared.length ? 1 : 0

This assumes the Mermaid node ids match top-level folder names under src/, which is a convention worth adopting anyway. Run it weekly, or in CI as a non-blocking job, and the drift becomes visible instead of silent.

Step 4: Keep curated diagrams small

Curated diagrams survive when they are cheap to maintain. Some rules of thumb:

  • One level of abstraction per diagram. Systems and people on one diagram, containers on another, components inside one container on a third. Never mix a database and a class name in the same picture.
  • Fewer than about 15 boxes. If it needs more, it is either the wrong level or should be generated.
  • No details that change weekly. Endpoint paths, class names, table names and version numbers belong in generated references or the code, not in a curated diagram.
  • Every arrow has a label. "Calls" is weak; "publishes OrderPlaced" is useful and is also something you can check.
  • Link decisions. If an arrow exists because of a choice ("we use a queue here so fulfillment can lag during sales"), link the architecture decision record. See the ADR guide for how to structure those.

Step 5: Review on a schedule, delete without guilt

Even with checks, curated diagrams need a periodic look. Put a recurring reminder on the owning team's calendar each quarter: open each diagram, compare it to the generated view, and either update the date or fix the drift. Fifteen minutes per diagram is typical.

And delete diagrams that nobody will own. A missing diagram sends people to the code or to a generated map, which is at least accurate. A wrong diagram sends them confidently in the wrong direction. If a diagram has no owner and no reviewed date in the last year, archive it with a note pointing to the generated view.

A checklist you can adopt this week

  • Find every architecture diagram your team links to and list them with owner and last-updated date.
  • Convert the two or three most-used ones to Mermaid or Structurizr DSL in the repo.
  • Add owners and a "last reviewed" line to each.
  • Pick a generator for your stack and produce a collapsed module graph.
  • Compare the generated graph with the curated diagram and write down every surprise.
  • Turn the most important intended boundary into a lint rule that fails CI.
  • Archive diagrams nobody will own.

Where generated maps fit day to day

The rules above handle enforcement. Day-to-day understanding is a different need: a new engineer, a reviewer, or an AI coding agent wants to see the current shape of the code without running a toolchain first. That is the case for maps that are built from the code every time you look, rather than diagrams that are drawn once.

OpenVisio's Explore space is one example of this approach: you paste a repository URL, it parses the code in your browser with tree-sitter, and the Architecture view shows the module and group graph as it exists in the current commit. The Tour adds a guided walkthrough with an isometric diagram of the same structure, which pairs well with a small curated context diagram that explains the intent. Whatever tooling you choose, the principle is the same: let humans curate the why, let machines derive the what, and check one against the other so neither drifts unnoticed.

architecture diagramsdiagrams as codedocumentationsoftware architecture

See it on your own repo

Paste a GitHub URL and get the day-one walkthrough — free, in your browser.

Index a repo

Keep reading