C4 Model and Docs-as-Code
Most architecture diagrams lie. Not on purpose, they simply go stale. The C4 Model and Docs-as-Code turn that around: four clean levels of abstraction, kept as a text source in the same Git repo as the code, reviewed in the pull request and rendered on the site. So the diagram ages with the code instead of against it.
The problem: diagrams that lie
The picture in Confluence is familiar: an architecture diagram, drawn in Visio or PowerPoint, last touched two years ago. It shows three services that no longer exist, and none of the five that have appeared since. Nobody trusts it, so nobody looks, and because nobody looks, nobody maintains it.
The reason is always the same: the documentation lives next to the code, not with it. It sits in a different tool, behind a different login, with no review gate. A code change forces no one to redraw the picture. Diagram and reality drift apart, quietly, guaranteed, from day one. That drift is a form of architecture debt, the same one that System Architecture addresses as a practice.
C4 Model: architecture in four zoom levels everyone understands
C4 is a practice, not a tool. A software system is described at four nested levels of abstraction (Context, Container, Component, Code), zooming from "who talks to the system" down to "which classes". A hierarchy instead of one-diagram-for-everything. It was created by Simon Brown (c4model.com). The idea behind it is a map with zoom levels: first the country, then the city, then the street, never everything at once.
Level 1: System Context
The top level shows the system as a single black box, surrounded by the people who use it and the external systems it talks to. No technology, no inner building blocks, just: who uses this? What does it talk to? This level is meant for everyone, non-technical readers included.
C4Context
title System context of an example platform
Person(customer, "Customer")
System(platform, "Example platform")
System_Ext(sso, "SSO / IdP")
System_Ext(pay, "Payment provider")
System_Ext(mail, "Email delivery")
Rel(customer, platform, "uses", "HTTPS")
Rel(platform, sso, "authenticates")
Rel(platform, pay, "books through")
Rel(platform, mail, "sends through")
One box in the middle, one actor, three external systems. Nothing more. Anyone reading this picture understands in ten seconds where the system sits in the bigger picture, without knowing a line of code.
Level 2: Container
Opening that same box one level deeper reveals the containers. In C4 a container is "an application or a data store … something that needs to be running in order for the overall software system to work" (c4model.com/abstractions/container), a deployable unit with its own technology tag: the web app, the API, the database, the worker.
C4Container
title Containers of an example platform
Person(customer, "Customer")
System_Boundary(platform, "Example platform") {
Container(web, "Web app", "Browser SPA")
Container(api, "API", "Backend")
Container(worker, "Worker", "Async jobs")
ContainerDb(db, "Database", "PostgreSQL")
}
System_Ext(sso, "SSO / IdP")
Rel(customer, web, "visits", "HTTPS")
Rel(web, api, "calls", "JSON")
Rel(api, db, "reads/writes", "SQL")
Rel(api, worker, "queues jobs")
Rel(worker, db, "reads/writes", "SQL")
Rel(api, sso, "authenticates", "OIDC")
The same system, one level deeper: the single black box from level 1 becomes four deployable units plus their connections. Every arrow carries a protocol, every box a technology, enough detail for an architecture conversation, little enough to fit on one slide. Where these container boundaries come from is rarely chance: they often follow the team boundaries that Conway's Law describes, and the asynchronous worker path is the entry point into an Event-Driven Architecture.
Level 3 and 4: Component and Code, deliberately brief
Below the container come the components (the building blocks inside a deployable unit) and, below those, the code (classes, interfaces). We keep these levels deliberately short here, and that is a practice statement, not an omission.
- Component only pays off for the few genuinely complex containers. For the rest, this level goes stale faster than it creates value.
- Code is almost never drawn by hand. The IDE generates the class diagram in seconds and always keeps it current. A hand-drawn code diagram is out of date the moment a file is saved.
More on this below under "When to stop".
Docs-as-Code: the workflow
For a diagram not to lie, it has to live where the code lives. That is Docs-as-Code: the diagram source is plaintext (Mermaid markup), sits in the same Git repo next to the code, is diffable (no binary blob you can only open in the tool) and runs through the same review gate as any code change.
flowchart TB
Src["diagram.md<br/>(source in Git)"]
PR["Pull Request<br/>(reviewed too)"]
CI["CI renders<br/>the diagram"]
Site["Site shows<br/>the diagram"]
Src -->|"committed next to code"| PR
PR -->|"merged"| CI
CI -->|"published"| Site
Site -.->|"change in the same PR"| Src
The effect: whoever changes the code changes the diagram in the same pull request, otherwise it shows up in review. The same "everything in Git" logic carries GitOps and the structured decisions in RFCs and ADRs.
When to stop, don't maintain every level
The most important practice statement is in the C4 docs themselves: "you don't need to use all 4 levels of diagram; only those that add value. The system context and container diagrams are sufficient for most software development teams" (c4model.com/diagrams).
As a rule of thumb:
- Context: almost always worth it. Cheap to maintain, rarely changes, explains the system to everyone.
- Container: almost always worth it. The workhorse, the level where architecture decisions become visible.
- Component: rarely. Only for the one genuinely complex container, and only while it stays complex.
- Code: almost never. Leave it to the IDE.
Maintenance cost against half-life: the deeper the level, the faster it goes stale and the more expensive it is to keep current. A diagram nobody maintains is worse than none, because it lies. Whether these diagrams actually get pulled along in review is a question of lived delivery discipline.
Pitfalls
- A "container" is not Docker. The C4 term is older and broader than the Docker hype. It means any deployable runtime unit (an SPA, a backend service, a database), whether or not it ever ends up in a Docker image. Simon Brown himself calls it "unfortunate that containerisation has become popular", because many now reflexively equate "container" with Docker (c4model.com/abstractions/container).
- Mixing levels. Keep each level clean: no internal building blocks on Context, no classes on Container. The moment a diagram blends two levels, it becomes the notation sprawl nobody reads.
- Maintaining diagrams to death. More diagrams isn't more clarity. Draw only what adds value (see above), the rest is maintenance debt.
- Mermaid isn't the only C4 tool. Mermaid is ideal for rendering diagrams lightweight inside Markdown. For teams wanting to run C4 strictly by the model (one model, many views, automatically consistent), Structurizr, Simon Brown's own diagram-as-code tool, is the alternative. For most teams Mermaid is enough; Structurizr is the deep dive, not a must.
Related topics
- System Architecture: the overarching architecture practice that C4 visualises.
- RFCs and ADRs: the decision docs to C4's structure docs; both live as Docs-as-Code in Git.
- Domain-Driven Design and Bounded Context: bounded contexts often map 1:1 onto C4 containers.
- GitOps: shares the "everything in Git" principle with Docs-as-Code.
- Strangler Fig: C4 diagrams make the step-by-step rebuild of a legacy system visible.
Reference Guide
- Simon Brown C4 Model. The canonical definition of the four levels and the notation. (2018). c4model.com
- Mermaid Mermaid. Diagrams as text, rendered straight in Markdown. (2014). mermaid.js.org
- Structurizr (Simon Brown) Simon Brown's own diagram-as-code tool for "proper" C4 beyond simple diagrams. structurizr.com
Ask AI
These links open external AI services, the conversation and its content are sent to their providers.