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.