DEV Community

Libme
Libme

Posted on

The Architecture Diagram in Your Wiki Is Lying: How to Generate One From the Code Instead

The fastest way to mislead a new hire is to hand them a hand-drawn architecture diagram from the wiki. If it was accurate the day it was drawn, it stopped being accurate the first time someone merged a PR nobody re-drew. The fix is not "draw it more carefully" — it's to stop drawing it by hand and generate it from the thing that's actually true: the code. Below is how to decide which parts to generate, which tool fits which language, and how to wire it into CI so the diagram fails the build when it drifts.

A commenter on an earlier post about CI pricing pointed out that architecture diagrams are what actually make a large repo legible, and that got me testing the generate-from-code approach across a few projects. This post is what held up.

Why do hand-drawn architecture diagrams go stale so fast?

Because they live in a different system from the code, and nothing forces the two to agree. A diagram in Confluence, a Miro board, or a .png checked into /docs has no relationship to import statements or service definitions. When someone splits a service in two, the code compiles, tests pass, the PR merges — and the diagram still shows one box. There is no failing signal. Six months later the diagram is confidently wrong, which is worse than absent: a new engineer trusts it and debugs against a mental model that hasn't existed since Q1.

The generated-diagram approach closes that gap by making the diagram a function of the source. If it's derived from import graphs, module boundaries, or an infrastructure manifest, then the only way for it to be wrong is for the code itself to be wrong — and now the diagram drifting is a code review comment, not an archaeology project.

The takeaway: a diagram you draw is a claim; a diagram you generate is a report. Only one of them can go stale silently.

Which layer of the diagram should you actually generate?

Not all of an "architecture diagram" can come from code, and pretending otherwise produces a hairball. Split it into three layers and generate the two that are mechanical:

  • Module/dependency graph (fully generatable): which files or packages import which. This is deterministic and the highest-value layer for onboarding, because it answers "if I touch this, what breaks?"
  • Deployment/infrastructure graph (mostly generatable): services, queues, databases, and their wiring — derivable from a docker-compose.yml, Terraform state, or Kubernetes manifests.
  • Intent/context (not generatable, and that's fine): why the boundaries are where they are. This is the one layer worth a human maintaining, and it's short — a few sentences per boundary, not a canvas.

Trying to auto-generate the intent layer is how you get a 400-node graph nobody reads. Generate structure, write intent, and keep them in separate files.

The takeaway: generate what's mechanical, hand-write only the "why," and never let the two blur into one lovingly-hand-placed diagram.

How does each tool actually work?

Here's the same job — "turn my repo into a diagram" — answered by the tools I've actually run, as of mid-2026.

Tool Ecosystem Input it reads Output Best at Real limitation
Mermaid Language-agnostic (you feed it text) Text you write or generate Diagram rendered inline in Markdown/GitHub Committing a diagram that renders in the PR itself It doesn't read your code — something else has to produce the text
dependency-cruiser JS/TS import/require graph Graphviz dot → SVG, plus a rules engine Enforcing layering rules, not just drawing Config has a learning curve; JS/TS only
madge JS/TS Module graph SVG/DOT, circular-dep list Fast "is there a cycle?" check Thinner than dependency-cruiser on rules
pydeps Python Import graph SVG/DOT Zero-config Python module maps Large projects need --max-bacon tuning or it's a blob
Structurizr DSL (C4) Language-agnostic A DSL you maintain C4 model diagrams, multiple views Multi-level system/container/component views The DSL is hand-maintained — it's diagrams-as-code, not derived-from-code
go-callvis / go mod graph Go Call graph / module graph Graphviz Go call and dependency structure Call-graph output gets noisy fast on big binaries

The important distinction hiding in that table: derived-from-code tools (dependency-cruiser, madge, pydeps) read your actual source and cannot lie about it. Diagrams-as-code tools (Mermaid by hand, Structurizr DSL) are still authored by a human — they version and review nicely, but they can drift, just more slowly and visibly than a Miro board. Know which kind you're adopting.

Wiring it into CI so drift fails the build

The generated SVG is nice, but the real payoff is treating architecture as an assertion. dependency-cruiser, for example, isn't just a drawing tool — it's a rules engine. You declare the boundaries you intend, and it fails when the code violates them.

Here's a minimal rule that forbids your UI layer from importing the database layer directly:

// .dependency-cruiser.js
module.exports = {
  forbidden: [
    {
      name: 'no-ui-to-db',
      comment: 'UI must go through the service layer, never straight to db',
      severity: 'error',
      from: { path: '^src/ui' },
      to:   { path: '^src/db' },
    },
    {
      name: 'no-circular',
      comment: 'Circular dependencies make the module graph unreadable',
      severity: 'error',
      from: {},
      to:   { circular: true },
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Then two lines in the pipeline — one to enforce, one to publish the picture:

# fail the build if an architectural boundary was crossed
npx depcruise src --config .dependency-cruiser.js

# regenerate the diagram artifact on every main build
npx depcruise src --include-only "^src" --output-type dot \
  | dot -T svg > docs/architecture.svg
Enter fullscreen mode Exit fullscreen mode

The first command is the part that matters. Now a PR that quietly makes the UI reach into the database doesn't merge — the same CI you're already paying for turns "the diagram drifted" into a red check. The published SVG is just a bonus that's now guaranteed current because it's regenerated from the same source the check passed against.

The takeaway: the diagram's value isn't the picture, it's the failing test underneath it.

Where do LLMs fit — and where do they burn you?

The tempting move is to paste the repo into an LLM and ask for a Mermaid diagram. It works impressively for a first-draft overview of an unfamiliar codebase, and it's genuinely good at the intent layer — summarizing why a boundary probably exists from reading the code. Where it burns you is precision: an LLM will confidently draw an edge that isn't there or miss one that is, because it's inferring structure, not parsing it. That's the exact failure mode — silent, plausible wrongness — you adopted generation to escape.

The workflow that actually holds: use a parser (dependency-cruiser, pydeps) for the structural graph that must be correct, and use an LLM to write the prose that explains it. If you want the managed, always-on version of this, a tool that regenerates the structural graph from the repo on every push is the one that keeps the diagram honest without anyone remembering to run it — just make sure the edges come from a parser, not a language model.

The takeaway: let a parser draw the edges and an LLM write the caption, never the other way around.

FAQ

How do I generate an architecture diagram from a GitHub repository automatically?
Add a dependency analysis tool for your language (dependency-cruiser for JS/TS, pydeps for Python) as a CI step that runs on every push to main, and have it output an SVG committed to /docs or published as a build artifact. Because it reads the actual import graph, it regenerates correctly without anyone maintaining it.

Why do architecture diagrams become outdated?
Because a hand-drawn diagram lives in a separate system from the code and nothing forces them to stay in sync — the code can change freely while the diagram sits untouched, so it drifts silently until someone notices it's wrong months later.

Can I trust an AI to generate an architecture diagram from my code?
For a rough first-pass overview, yes; for a diagram other people will make decisions from, no. LLMs infer structure and will occasionally invent or drop a dependency. Use a parser for the edges that must be accurate and reserve the AI for explaining the diagram, not drawing it.

Bottom line

If your repo is JS/TS, adopt dependency-cruiser first — you get the diagram and, more importantly, enforceable boundaries in the same tool. Python teams get most of that value from pydeps plus a CI check. Reach for Structurizr's C4 DSL when you need multi-level views for a system big enough that one graph can't hold it, and accept that it's diagrams-as-code you'll maintain by hand. Whatever you pick, generate the structure from source and hand-write only the intent — the moment a human is placing boxes to represent import relationships, you're back to drawing a diagram that will lie to the next new hire.

Related reading

Top comments (0)