Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 4.7 of 4.8 · Intermediate · 22 min read

Diagrams as code and keeping documentation current

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Most architecture documentation doesn't fail because it was badly written. It fails because it goes stale. In this lesson you'll learn how writing diagrams as text, and a few simple habits, keep documentation true for the life of a system.

Imagine a family recipe book where every change is pencilled in by whoever cooks, with no record of who changed what. After a few years, nobody trusts it. Now imagine the recipes are typed, and every change is suggested, checked by a second cook, and dated. And the printed book is reprinted automatically after each approved change. Diagrams as code applies that discipline to architecture diagrams.

Diagrams as code means writing a diagram as a short text file and letting a tool lay it out and draw it. Instead of dragging boxes on a canvas, you write something like ordering arrow database, and the tool produces the picture. Why bother? A text file has a history: who changed it, when and why. And when a developer adds a new dependency, the diagram change appears in the same pull request as the code, so reviewers can question the new arrow when it's cheapest to change.

Four languages dominate, and the section writes the same food-delivery platform in each. PlantUML, first released in 2009, covers the common diagrams of the Unified Modeling Language, and its sequence diagrams are particularly good. Mermaid, from 2014, renders directly in Markdown files on GitHub and GitLab, so a diagram in a read me file needs no build step at all. Its layout control on large diagrams is limited. Structurizr's language, created by Simon Brown alongside C4, is different in kind. It doesn't describe a diagram. It describes a model, and then defines views of it. And D2, open-sourced in 2022, aims at clean, modern output and readable syntax. The trade-off with all of them: automatic layout is convenient, but sometimes ugly.

Here's why a model matters. On the left is one workspace file. Each person, system and container is defined once, with its name and technology. From that one model, the tool generates the views: system context, containers, and deployment for each environment. Each view can be exported as images, as other diagram languages, or as a documentation site with the decision records. So rename the ordering service once in the model, and every view changes with it. In a drawing tool, two boxes with the same label have no connection, and you must fix every diagram by hand.

Once documentation is text in the repository, it can follow exactly the same workflow as the code. You edit it on a branch, and it's reviewed in a pull request alongside the code change. Continuous integration checks then render every diagram, and check the links. A broken link, an invalid diagram or an overdue review date fails the build, and sends it back. Once it's merged to main, the site is rebuilt automatically. Nobody has to remember to export a picture and upload it to a wiki. So readers always see the documentation that matches the code on main. And when they raise issues, those start the next change.

Tools don't keep documentation true. Habits do. First, every document has a named owner, normally the team that owns the code. Documents owned by everyone are maintained by no one. Second, review documentation with code changes. A pull request template that asks whether an interface, a dependency or a decision changed catches most drift as it happens. Third, give each page a last-reviewed date. A page that says last reviewed 2023 is honest. A page that silently describes the system of 2023 is a trap. Fourth, link both ways: pages point to the code, and code comments point to decision records. And fifth, delete stale material, because wrong documentation is worse than none. The exception is decision records. They are history, so they are superseded, never deleted.

Finally, a few common mistakes. Choosing a tool before deciding what to document: the tool matters far less than ownership and review. Committing rendered images without their sources, so the two drift apart. And treating diagrams as code as an excuse to skip titles, keys and labels. The C4 notation rules still apply to generated diagrams.

The full section shows the platform written in all four languages, a sample pipeline, and a checklist for documentation that stays current. Next, in Section four point eight, we turn from documents to people. See you there.

What you need to know

What diagrams as code means

Diagrams as code means writing a diagram as a short text file in a small language and letting a tool lay it out and draw it. Instead of dragging boxes on a canvas, you write ordering -> db: SQL, and the tool produces the picture. The text file lives in the code repository and is treated like code: versioned, reviewed, checked by automated builds and published by the pipeline.

It is one part of docs-as-code, the practice, championed by the Write the Docs community, of producing documentation with the same tools and workflow as software: issue trackers, version control (usually Git), plain-text markup such as Markdown or AsciiDoc, code review and automated tests. The previous sections produced the material: views and C4 diagrams, UML where precision matters, ADRs and an arc42 structure. This section is about keeping that material accurate for the life of the system, which is where most architecture documentation fails. The reasons it fails were set out in why and how much to document: stale wikis, big documents nobody owns and diagrams that no longer match the system.

Why put diagrams in text

Version control. A diagram in text has a history. You can see who changed it, when and why, and compare any two versions line by line; a binary image or a file in a drawing tool's format shows only that something changed. You can also branch it: the diagram for a proposed change lives on the same branch as the code that implements it.

Review in pull requests. When a developer adds a new dependency between services, the change to the diagram appears in the same pull request as the code, as a readable text diff. Reviewers can ask whether the new arrow is wanted, which is an architecture review at the moment it is cheapest.

Consistency. The tool applies the same shapes, fonts and layout rules to every diagram, so diagrams drawn by twenty people still look like one set. With a modelling language (below), the same element appears with the same name and description in every view.

Generation. Text can be produced by programs. Diagrams can be generated from the code itself (dependency graphs), from infrastructure-as-code (deployment diagrams), from API specifications or from a central model, and then checked automatically against the system.

The trade-offs are real. Automatic layout is convenient but sometimes ugly, and fighting a layout engine to place one box can take longer than drawing it; most tools offer hints such as direction or grouping, not exact positions. Text languages have a learning curve for people who do not code, such as product managers. And a beautiful sketch on a whiteboard during a design discussion is still the fastest way to think; diagrams as code are for the diagrams you intend to keep.

Four languages, one example

Four languages dominate. Each example below describes part of the same food-delivery platform.

PlantUML, created by Arnaud Roques and first released in 2009, is the oldest of the four. It covers all the common UML diagrams (sequence, component, deployment, state, activity, class) and many others, and uses Graphviz to lay out most diagram types. The C4-PlantUML library adds C4 shapes. Its sequence diagrams are particularly good, which makes it a natural choice for the runtime scenarios of UML essentials.

@startuml
title Place an order (happy path)
actor Customer
participant "Customer app" as App
participant "Ordering service" as Ord
participant "Payment provider" as Pay
queue "Order events topic" as MB

Customer -> App : Tap "Pay"
App -> Ord : POST /orders (HTTPS)
Ord -> Pay : Authorise payment
Pay --> Ord : Authorised
Ord ->> MB : OrderPlaced (async)
Ord --> App : 201 Created
@enduml
PlantUML: a sequence diagram of placing an order; ->> draws an asynchronous message and --> a reply

Mermaid, created by Knut Sveidqvist in 2014, is written in JavaScript and renders in the browser. Its biggest advantage is reach: GitHub has rendered Mermaid blocks in Markdown files since February 2022, and GitLab and many wiki and note-taking tools do the same, so a diagram in a README needs no build step at all. It supports flowcharts, sequence, class, state and entity-relationship diagrams, among others, and has experimental C4 support. Its layout control is limited for large diagrams.

flowchart LR
    app["Customer app<br/>Kotlin, Swift"]
    gw["API gateway"]
    ord["Ordering service<br/>Java"]
    db[("Order database<br/>PostgreSQL")]
    topic[["Order events<br/>Kafka topic"]]
    disp["Dispatch service<br/>Go"]
    notif["Notification service<br/>Node.js"]

    app -->|HTTPS/JSON| gw
    gw -->|HTTPS/JSON| ord
    ord -->|SQL| db
    ord -->|publishes OrderPlaced| topic
    disp -->|consumes| topic
    notif -->|consumes| topic
Mermaid: a container-level flowchart; [( )] draws a database cylinder and labels sit on the arrows; the topic is drawn as a container, as Section 4.3 recommends

Structurizr DSL, created by Simon Brown alongside the C4 model, is different in kind. It does not describe a diagram; it describes a model (people, software systems, containers, components and the relationships between them) and then defines views of that model. Each element is defined once, with its name, description and technology, and appears in every view that includes it. Relationships between containers are automatically implied at the system level, so the context view needs no separate arrows. The DSL can also attach Markdown documentation and a folder of ADRs to the workspace, and the tooling can export views to PlantUML, Mermaid, D2 and other formats.

workspace "Food delivery" {
    model {
        customer = person "Customer" "Orders food from local restaurants."
        payments = softwareSystem "Payment provider" "Takes card payments." "External"
        platform = softwareSystem "Food-delivery platform" {
            app = container "Customer app" "Ordering and tracking." "Kotlin, Swift"
            gateway = container "API gateway" "Authenticates and routes requests." "Managed API gateway"
            ordering = container "Ordering service" "Accepts and tracks orders." "Java"
            db = container "Order database" "Orders and their states." "PostgreSQL" "Database"
            orderEvents = container "Order events" "Order lifecycle events." "Kafka topic" "Topic"
            dispatch = container "Dispatch service" "Assigns couriers." "Go"
        }
        customer -> app "Places and tracks orders using"
        app -> gateway "Makes API calls to" "JSON/HTTPS"
        gateway -> ordering "Routes requests to" "JSON/HTTPS"
        ordering -> db "Reads from and writes to" "SQL/TCP"
        ordering -> payments "Takes card payments using" "JSON/HTTPS"
        ordering -> orderEvents "Publishes order events to" "Kafka"
        dispatch -> orderEvents "Consumes order events from" "Kafka"
    }
    views {
        systemContext platform "Context" {
            include *
            autoLayout lr
        }
        container platform "Containers" {
            include *
            autoLayout lr
        }
        styles {
            element "Database" {
                shape cylinder
            }
            element "Topic" {
                shape pipe
            }
        }
    }
}
Structurizr DSL: one model and two views of the food-delivery platform; the context view is generated from the same definitions

D2 (Declarative Diagramming), open-sourced by Terrastruct in 2022, is the newest. It aims at clean, modern output and readable syntax, supports nested containers (written with braces and referred to with dotted names), and offers a choice of layout engines: dagre and ELK are bundled and free, and TALA, designed for software architecture diagrams, is a separate commercial engine.

direction: right

customer: Customer {shape: person}
payments: Payment provider

platform: Food-delivery platform {
  app: Customer app
  ordering: Ordering service
  db: Order database {shape: cylinder}
  events: Order events topic {shape: queue}
  dispatch: Dispatch service

  app -> ordering: Places order (HTTPS)
  ordering -> db: SQL
  ordering -> events: Publishes OrderPlaced
  dispatch -> events: Consumes OrderPlaced
}

customer -> platform.app: Places orders using
platform.ordering -> payments: Authorise (HTTPS)
D2: nested containers, shapes and labelled connections

Comparing the four

PlantUMLMermaidStructurizr DSLD2
DescribesOne diagram per fileOne diagram per blockA model plus views of itOne diagram per file
Strongest atUML, especially sequence diagramsDiagrams inside Markdown with no build stepConsistent C4 diagrams at several levelsAttractive general architecture diagrams
RendersJava tool or server; Graphviz for most layoutsIn the browser; native on GitHub and GitLabStructurizr tools; exports to other formatsCommand-line tool; choice of layout engine
Watch out forDated default look; layout control by hints onlyLimited layout control on large diagramsOnly C4 concepts; needs its own toolingBest layout engine is commercial

One model, many views

One model, many views: a Structurizr DSL workspace defines elements and relationships once; context, container and deployment views are generated from it and exported as images, other diagram languages or a documentation siteworkspace.dsl (one model)person Customersystem Platformcontainer OrderingJavacontainer Order DBPostgreSQLcontainer Order eventsKafka topic+ relationships, tags, stylesSystem context viewpeople and systemsContainer viewinside the platformDeployment viewper environmentgenerateSVG, PNGimagesPlantUML,Mermaid, D2Docs sitewith ADRsRename Ordering once in the model and every view and export changes with it.
A modelling language defines each element once; views and exports are generated, so they cannot disagree with each other.

Drawing tools and modelling tools

The difference between Structurizr and the other three is an instance of the distinction between drawing and modelling tools introduced in Section 4.3. A drawing tool (diagrams.net, Excalidraw, Visio, an online whiteboard, and also PlantUML, Mermaid and D2) knows about shapes, lines and text. A box labelled Ordering service in one diagram has no connection to the box with the same label in another. Rename the service and you must find and fix every diagram by hand; misspell it once and you have, as far as the tool knows, two services.

A modelling tool (Structurizr, Archi for ArchiMate models, Sparx Enterprise Architect for UML and SysML, Capella) keeps a model: a repository of elements and relationships with properties. Diagrams are views onto the model. Rename an element and every view changes. The model can be queried (which containers depend on the broker?), checked (no component in the customer app may call the database) and compared with the running system. The price is more up-front structure and, with the heavyweight commercial tools, licences, training and a tendency to model far more than anyone reads.

A practical rule: use drawing tools, text or graphical, for sketches, one-off explanations and diagrams that stand alone, such as a single sequence diagram in an ADR. Use a model when the same elements appear in many diagrams, when several teams contribute, or when you want to check the documentation against the code. Many teams combine them: a Structurizr model for the C4 structure and PlantUML or Mermaid for a few sequence and state diagrams.

Generating documentation in the pipeline

Once documentation is text in the repository, the continuous integration pipeline from Chapter 0 can build and check it like code. A typical pipeline renders every diagram (failing the build on a syntax error), checks links, builds a static documentation site with a generator such as MkDocs, Antora or Docusaurus, and publishes it when changes reach the main branch. Readers then always see the documentation that matches the code on main, and nobody needs to remember to export a picture and upload it to a wiki.

Further checks can be added as the team matures: a spell checker and prose linter (Vale is popular), a check that every ADR has a valid status, a warning when a page's last reviewed date is older than its review interval, and architecture checks that compare the documented dependencies with the actual ones in the code, which are a form of fitness function. Some teams generate parts of the documentation from the system itself: dependency graphs from the build, API reference from OpenAPI specifications, deployment diagrams from infrastructure-as-code.

name: architecture-docs
on:
  pull_request:
    paths: ["docs/**"]
  push:
    branches: [main]

jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # assumes plantuml, d2, lychee and mkdocs are available on the runner
      - name: Render diagrams (fails on syntax errors)
        run: |
          plantuml -tsvg docs/diagrams/*.puml
          for f in docs/diagrams/*.d2; do d2 "$f" "${f%.d2}.svg"; done
      - name: Check links
        run: lychee --offline docs/
      - name: Build site (warnings such as broken links fail the build)
        run: mkdocs build --strict
      - name: Publish
        if: github.ref == 'refs/heads/main'
        run: ./scripts/publish-docs.sh
A GitHub Actions workflow that renders diagrams, checks links, builds the documentation site on every pull request and publishes from main

The docs-as-code workflow

The docs-as-code workflow: documentation is edited on a branch, reviewed in a pull request, checked by CI, merged and published automatically; failed checks and reader issues loop backBranchedit .md, .dsl,ADR filesPull requestreviewed withthe code changeCI checksrender, lint,check linksMerge to mainapproved bydoc ownersPublishsite rebuiltautomaticallya broken link, invalid diagram or stale date fails the buildReadersalways see mainreaders raise issues, which start the next changeThe documentation follows exactly the same workflow as the code
Documentation changes travel the same path as code changes: branch, review, automated checks, merge and automatic publication.

Keeping documentation current

Tools do not keep documentation true; habits do. Five habits make the difference.

Ownership. Every document has a named owner, normally the team that owns the code it describes. In GitHub and GitLab a CODEOWNERS file can make that team a required reviewer for changes under its docs/ folder. Documents owned by everyone are maintained by no one.

Review with code changes. A pull request template that asks Does this change an interface, a dependency, a deployment or a decision? If so, which document or ADR did you update? catches most drift at the moment it happens. Some teams go further and block merges of features that lack documentation, a practice the Write the Docs guide mentions.

Last-reviewed dates. A small header on each page records its owner, the date it was last checked against reality and how often it should be reviewed. Readers can judge how far to trust it, and the pipeline can flag pages that are overdue. A page that says last reviewed 2023 is honest; a page that silently describes the system of 2023 is a trap.

Linking documentation and code. Pages point to the code they describe (paths, modules), code comments point to ADRs, and commit messages cite the records they implement. Links in both directions make it obvious which documents a change might affect.

Deleting stale material. Wrong documentation is worse than none, because readers act on it. When a page no longer describes the system and nobody will fix it, delete it or move it to a clearly marked archive. ADRs are the exception: they are records of history, so they are superseded or deprecated, not deleted. This is the living documentation idea that Cyrille Martraire developed in his 2019 book of that name: documentation that evolves at the same pace as the system, ideally because much of it is derived from the system.

---
title: "Ordering service: building block view (level 2)"
owner: "@ordering-team"
last_reviewed: 2026-09-14
review_every: 6 months
code: src/ordering/
decisions: [ADR-0007, ADR-0012]
---
Front matter at the top of a Markdown page: owner, review date and interval, and links to the code and decisions it describes

Checklist: documentation that stays current

Use this when setting up a repository's architecture documentation, and revisit it every few months.

  • Documentation lives in the repository it describesMarkdown or AsciiDoc pages, diagram sources and ADRs under docs/, not in a separate wiki that nobody updates in the same change.
  • Diagram sources are text and are rendered by the pipelineNo hand-exported images that silently fall behind their sources; the build fails on an invalid diagram.
  • Each element is defined once where diagrams overlapUse a model (such as Structurizr DSL) when the same containers appear in several diagrams.
  • Every page has an owner and a last-reviewed dateEnforced by CODEOWNERS and a pipeline check that warns when the review interval has passed.
  • Pull requests ask about documentationThe template asks whether an interface, dependency, deployment or decision changed and which document was updated.
  • Links are checked automaticallyBroken internal links and references to deleted files fail the build.
  • Docs point to code and code points to decisionsPaths in the documentation, ADR numbers in code comments and commit messages.
  • The published site is rebuilt from mainReaders always see the version that matches the deployed code, with no manual publishing step.
  • Stale pages are deleted or archivedPages nobody will fix are removed; ADRs are superseded or deprecated, never deleted.
  • At least one check compares documentation with realityFor example, a test that fails if a dependency appears in the code that the container diagram does not show.

Check your understanding

What can a reviewer see in a pull request when a diagram is code that they cannot see when it is an exported image?

A readable line-by-line diff of what changed, such as a new relationship between two services, in the same pull request as the code that introduced it.

Why does renaming a service cause more work with a drawing tool than with a modelling tool?

In a drawing tool each diagram holds its own copy of the label, so every diagram must be found and edited; in a modelling tool the element is defined once and every view picks up the new name.

Why should stale documentation be deleted rather than left in place, and what is the exception?

Readers act on what they read, so wrong documentation causes wrong decisions. ADRs are the exception: they record history and are marked superseded or deprecated instead.

Name two checks a documentation pipeline can run besides rendering diagrams.

Link checking, prose or spelling linting, a strict site build, a warning for overdue review dates, and comparing documented dependencies with those in the code.

Next · 4.8CommunicatingTailoring architecture to executives, product managers, developers, operations, security and auditors: explaining trade-offs, one-page summaries, following one request, design docs and RFCs, reviews, diagram design and building consensus.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “Our architecture diagrams live in a drawing tool and are always out of date. How would I move to diagrams as code without making non-developers unable to contribute?”