6 min read
On this page

Diagrams as Code

Image-based diagrams rot. Someone creates a diagram in Lucidchart or draw.io, exports a PNG, drops it into the docs, and within six months the diagram no longer matches the system. The source file is on someone's laptop — maybe. Nobody updates it because updating means finding the source, opening the tool, making the change, re-exporting, and replacing the image. So the diagram stays wrong, and eventually everyone ignores it. Diagrams as code solves this by treating diagrams like source code: plain text files that live in version control, diff cleanly in pull requests, render automatically, and stay in sync with the codebase because updating them is as easy as editing a text file.

The Problem with Image-Based Diagrams

Traditional diagramming tools produce binary outputs — PNG, SVG, or proprietary formats — that break every principle of maintainable documentation.

Problems with image-based diagrams:

  Version control   — Binary files do not diff. You cannot see what
                      changed between versions in a pull request.

  Ownership         — The source file lives in one person's account
                      on a SaaS tool. When they leave, access is lost.

  Discoverability   — Diagram sources are scattered across Lucidchart,
                      draw.io, Figma, and Google Slides. Nobody knows
                      where the source for a specific diagram lives.

  Updates           — Updating requires: find source, open tool, edit,
                      export, replace image in docs. This friction means
                      diagrams do not get updated.

  Collaboration     — Two people cannot edit the same diagram through
                      normal code review workflows.

  Automation        — Cannot auto-generate or auto-validate image-based
                      diagrams in CI/CD.

Diagrams as code eliminates all of these problems by making diagrams text files that live alongside the code they describe.

How Diagrams as Code Works

Instead of dragging boxes in a GUI, you write a text file that describes the diagram. A renderer converts the text to an image (SVG, PNG) either locally, in CI, or at render time in the documentation platform.

The workflow:

  1. Write diagram source in a text format (Mermaid, PlantUML, D2)
  2. Commit the source file to the repository
  3. The renderer produces an image (locally, in CI, or on the fly)
  4. The documentation embeds the rendered output
  5. Updates go through normal code review (pull requests, diffs)

The source file is the single source of truth. The rendered image is a build artifact, just like compiled code.

Mermaid

Mermaid is the most widely supported diagram-as-code tool. It renders natively in GitHub, GitLab, Notion, Obsidian, Docusaurus, and many other platforms. No plugins or build steps required in most environments.

Strengths

  - Native rendering in GitHub markdown (just use a mermaid code block)
  - Large ecosystem of supported diagram types
  - Active development and growing feature set
  - Low learning curve — readable syntax
  - Supported in most documentation platforms without configuration

Syntax Examples

Flowchart:

graph LR
    A[Client] -->|HTTPS| B[API Gateway]
    B -->|gRPC| C[User Service]
    B -->|gRPC| D[Order Service]
    C --> E[(PostgreSQL)]
    D --> F[(MongoDB)]
    D --> G[Redis Cache]

Sequence diagram:

sequenceDiagram
    participant C as Client
    participant G as API Gateway
    participant U as User Service
    participant DB as PostgreSQL

    C->>G: POST /login
    G->>U: ValidateCredentials(email, password)
    U->>DB: SELECT * FROM users WHERE email = ?
    DB-->>U: User record
    U-->>G: JWT token
    G-->>C: 200 OK + token

Limitations

  - Limited control over layout (the renderer decides placement)
  - Complex diagrams can produce messy layouts
  - Styling options are restricted compared to GUI tools
  - Not ideal for diagrams requiring precise positioning

Mermaid works best for diagrams with under 15 nodes. Beyond that, the auto-layout struggles and the output becomes hard to read.

PlantUML

PlantUML is the oldest and most feature-rich diagram-as-code tool. It supports a wider range of diagram types than Mermaid and gives more control over layout, but requires a Java runtime and is not natively supported in as many platforms.

Strengths

  - Extremely mature (first released in 2009)
  - Supports UML diagrams, Gantt charts, mind maps, and more
  - Better layout algorithm for complex diagrams
  - More control over styling and positioning
  - Large library of icons and sprites (AWS, Azure, GCP, Kubernetes)

Syntax Example

@startuml
package "Backend" {
    [API Gateway] as gw
    [User Service] as us
    [Order Service] as os
}

database "PostgreSQL" as db
queue "RabbitMQ" as mq

gw --> us : REST
gw --> os : REST
us --> db : JDBC
os --> db : JDBC
os --> mq : AMQP
@enduml

Limitations

  - Requires Java runtime (or a server/Docker container)
  - Not natively rendered in GitHub or most platforms
  - Syntax is more verbose than Mermaid
  - Needs a build step or plugin for most documentation setups

PlantUML is the better choice when you need complex UML diagrams, cloud architecture diagrams with vendor icons, or precise control over layout. It is worse when you want zero-config rendering in GitHub.

D2

D2 is the newest of the three major options, designed specifically to address the layout and styling limitations of Mermaid and PlantUML. It produces cleaner output for complex diagrams and has a more modern design philosophy.

Strengths

  - Superior layout engine (dagre, ELK, or TALA)
  - Clean, readable syntax
  - Better default styling than Mermaid or PlantUML
  - Supports themes and custom styling
  - Handles complex diagrams without layout degradation
  - Standalone binary with no runtime dependencies

Syntax Example

client: Client { shape: person }
gateway: API Gateway

services: Backend Services {
    users: User Service
    orders: Order Service
}

data: Data Stores {
    pg: PostgreSQL { shape: cylinder }
    redis: Redis { shape: cylinder }
}

client -> gateway: HTTPS
gateway -> services.users: gRPC
gateway -> services.orders: gRPC
services.users -> data.pg
services.orders -> data.redis

D2's syntax reads naturally and supports nested grouping, custom shapes, and styling. The layout engine handles positioning automatically.

Limitations

  - Newer tool with a smaller community
  - Not natively supported in GitHub or most platforms (needs a build step)
  - Fewer diagram types than PlantUML
  - Ecosystem is still growing

D2 is the best choice when diagram quality and layout matter and you do not mind a build step. It produces the cleanest output of the three tools for architecture diagrams.

Choosing a Tool

Criterion           Mermaid        PlantUML       D2
──────────────────────────────────────────────────────────
GitHub native       Yes            No             No
Build step needed   No (usually)   Yes            Yes
Layout quality      Adequate       Good           Excellent
Diagram types       Many           Most           Growing
Syntax simplicity   Simple         Moderate       Simple
Styling control     Limited        Moderate       Good
Runtime deps        None           Java           None
Community size      Large          Large          Growing
Best for            Quick inline   Complex UML    Clean arch

If your documentation lives in GitHub and you want zero setup, use Mermaid. If you need complex UML diagrams or cloud vendor icons, use PlantUML. If you want the best-looking output and do not mind a build step, use D2.

Many teams use Mermaid for inline documentation and D2 or PlantUML for published architecture documentation. The tools are not mutually exclusive.

Embedding in Markdown

Mermaid renders natively in GitHub, GitLab, Notion, and Obsidian using fenced code blocks with the mermaid language tag. No plugins or build steps needed. Docusaurus, MkDocs, and Confluence support Mermaid through plugins.

For PlantUML and D2, render in CI and embed the resulting SVG or PNG. The markdown references the rendered image, and the build pipeline keeps it in sync with the source.

Auto-Rendering in CI

The key advantage of diagrams as code: you can automate rendering so diagrams are always up to date with the source files.

Basic CI Pipeline

Steps:
  1. Developer edits diagram source file (.mmd, .puml, or .d2)
  2. Developer commits and opens a pull request
  3. CI pipeline detects changed diagram sources
  4. CI renders diagram sources to SVG/PNG
  5. CI commits rendered images back to the branch (or publishes as artifacts)
  6. Documentation site uses rendered images

Tools for CI rendering:
  Mermaid:  mmdc (mermaid-cli) via npx @mermaid-js/mermaid-cli
  PlantUML: plantuml.jar or plantuml Docker image
  D2:       d2 binary (single binary, no dependencies)

You can also add CI validation: syntax checks, render-without-errors checks, and freshness checks that verify rendered images are newer than their source files.

Why Image-Based Diagrams Rot

The fundamental problem is friction. Every additional step between "I changed the system" and "the diagram reflects the change" makes it less likely the diagram gets updated.

Image-based workflow (high friction):
  1. Remember that a diagram exists for this system
  2. Find the source file (where is it? who made it?)
  3. Open the diagramming tool (do you have a license?)
  4. Make the change
  5. Export to PNG/SVG
  6. Find where the image is used in the docs
  7. Replace the image
  8. Commit

Code-based workflow (low friction):
  1. Edit the text file next to the code
  2. Commit
  [CI handles the rest]

The code-based workflow has two steps. The image-based workflow has eight. Over time, the eight-step process loses to human nature, and diagrams drift from reality. Within a year, nobody trusts the diagrams and everyone stops looking at them.

Diagrams as code does not eliminate this problem entirely — someone still has to edit the source file. But it reduces the friction enough that updates actually happen, especially when diagram source files live in the same directory as the code they describe.

Organizing Diagram Files

Store diagram sources either in a /docs/architecture/ directory or co-located with the code they describe (e.g., architecture.mmd next to the service's source). Co-location increases the chance diagrams get updated when the code changes, because the developer sees them in the same directory.

Common Pitfalls

  • Choosing a tool and then fighting its layout engine. If the auto-layout produces ugly results for your diagram, try a different tool or split the diagram. Do not spend hours tweaking coordinates.
  • Not committing rendered output. If your platform does not render natively, commit the rendered images alongside the source so readers see the diagram without running a build.
  • Storing diagram sources separately from the docs. If the source file is in a different repo or directory from the documentation, they will drift apart.
  • Over-engineering the CI pipeline. Start simple: render on commit. Add validation later. Do not block your first diagram on building a perfect pipeline.
  • Ignoring Mermaid's limitations. Mermaid is convenient but struggles with complex diagrams. If your diagram has more than 12-15 nodes, evaluate D2 or PlantUML.
  • Creating code-based diagrams that nobody can read. The source file should be readable on its own, not just when rendered. Use clear names, add comments for complex sections, and keep the source organized.
  • Treating diagrams as code but not reviewing them like code. If diagram source files change in a PR, review them. Check that labels are accurate, connections are correct, and the abstraction level is consistent.

Key Takeaways

  • Image-based diagrams rot because updating them has too much friction. Code-based diagrams survive because updating them is a text edit and a commit.
  • Mermaid for zero-config GitHub rendering, PlantUML for complex UML with vendor icons, D2 for the best layout quality.
  • Store diagram source files in version control, next to the code or documentation they describe.
  • Automate rendering in CI so diagrams are always in sync with their source.
  • Diagram source files should diff cleanly in pull requests, enabling normal code review for visual documentation.
  • Start with Mermaid and move to other tools when you hit its limitations. Do not over-engineer from the start.