ADR 0020: C4 views, PlantUML conventions, and shared diagram style
Ratification
Adopted under ADR 0018. Link the discussion Issue and merge PR when they exist. If not, use git history as the record.
- Discussion Issue: none linked for this merge
- Merge PR: see git history for this file
- Accepted: 2026-04-12
Context
This section explains what we are standardizing (C4 thinking, PlantUML, shared visuals) and why that matters before the concrete rules in Decision.
Why draw architecture at all?
Software systems live in people’s heads, in code, and in running infrastructure—but those representations answer different questions. Code shows how behavior is implemented; it does not, by itself, give newcomers a stable map of who uses the system, what major parts exist, or where responsibilities sit. Without shared structural pictures, teams rely on oral tradition, which drifts as people rotate and features accumulate. Explicit architecture views reduce misunderstandings in planning, security review, onboarding, and incident response: they are a deliberate compression of complexity, not decoration.
What the C4 model is (and what it is not)
The C4 model (popularized by Simon Brown and described at c4model.com) is a hierarchy of zoom levels for describing software architecture:
- Level 1 — System context: the system in its environment—users, external systems, key dependencies. Answers “What is this thing, and who interacts with it from the outside?”
- Level 2 — Containers: high-level technical building blocks that could be deployed or replaced separately (applications, databases, message buses, major processes). Answers “What are the main runtime pieces and how do they connect?”
- Level 3 — Components: major parts inside a container—layers, modules, services. Answers “How is responsibility divided inside that deployable unit?”
- Level 4 — Code (optional in many teams): classes or files. Often left to the codebase and IDEs; we do not require a standing L4 diagram for every feature.
The point is separation of concerns by zoom: each level hides detail that belongs at a finer grain, so diagrams stay readable. Mixing “a user clicks a button” with “this SQL migration file” on one canvas usually produces noise. C4 is not a formal ISO standard with a single mandated notation; it is a mental model and vocabulary that many teams have found compatible with agile delivery and incremental updates.
Why sequence diagrams alongside structure?
C4 views are mostly static: boxes and lines show ownership and dependency. They do not fully capture order of operations across time—who calls whom, validation order, alternate failure paths. Sequence diagrams (UML interaction diagrams) complement structural views by describing message flow for concrete scenarios (for example “create note” or “handle conflict”). Together, structure plus sequences give a fuller picture without replacing tests or code.
Why standardize on one textual notation (PlantUML)?
Diagrams can be drawn in many tools. A textual source (here, PlantUML) aligns with docs as code: the same review, branching, and diff workflows as for application code. Generated images (for example SVG) stay reproducible from source; style can be centralized. Alternatives such as binary drawing files or unconstrained wiki embeds are harder to diff, easier to silently desynchronize from the repository, and harder to lint or regenerate in CI. This ADR does not claim PlantUML is the only good tool in the industry—it claims that one chosen tool for this repo is preferable to many.
Why a shared visual style (colors, fonts, arrows)?
Consistency is not vanity. When every author picks arbitrary colors, readers must re-learn a legend on every page. A small, fixed palette tied to roles (people, APIs, data stores, infrastructure) lowers cognitive load and makes printed or embedded figures look like one publication, not a collage. Shared typography and line weight improve legibility in HTML and when slides are exported from the same sources.
Why this repository cares now
This project already keeps PlantUML under docs/uml/, merges a shared skin from
docs/uml/include/style.puml when rendering via Kroki, and references generated SVG from HTML. The gap was a
written policy: what C4 means here, how it maps to PlantUML shapes, and how to evolve the skin
without one-off diagrams drifting away. This ADR turns an informal habit into an explicit contract so future
contributors know what “good” looks like and why.
Decision
All architecture and interaction diagrams that live under docs/uml/ are authored in
PlantUML using standard UML and structural elements. We describe systems using the
C4 model as the mental map (levels 1–3), implemented with PlantUML primitives—not as a mandate
to use the third-party C4-PlantUML macro library, which would complicate single-file Kroki rendering.
Visual consistency comes from the shared file docs/uml/include/style.puml, injected at render time by
scripts/regenerate_docs.py.
Scope of notation (UML / PlantUML only)
-
In scope for this repo: PlantUML sources (
.puml) and SVG outputs underdocs/uml/rendered/, referenced from HTML such asdocs/internal/system-design.html. - Out of scope: Mermaid, Graphviz DOT, draw.io, or other diagram sources for the same canonical architecture views—unless a future ADR changes this. (Informal sketches in issues or whiteboards are unconstrained.)
-
Use PlantUML’s built-in layout (“smart” automatic placement). Prefer
left to right directionfor structural views so wide diagrams read naturally on documentation pages. Usetop to bottom directionwhen a single container shows vertical layers (API → services → persistence): orthogonal routing and hidden alignment links then stack internals cleanly without wide horizontal sprawl.
C4 levels mapped to PlantUML elements
Follow C4 naming for titles and intent; map elements as below so colors from the shared skin apply predictably.
| C4 level | Intent | PlantUML |
|---|---|---|
| L1 System context | People and external systems around the product |
actor for people; package to group external apps; rectangle for the
system under design; database when a store is iconic at this zoom level
|
| L2 Containers | Deployable apps, data stores, major technical building blocks |
node for a process or host boundary; [Container] rectangles inside;
database for stores; package for ancillary stacks (for example observability)
|
| L3 Components | Major parts inside a container (layers, modules) |
package per layer or bounded context; [Name] components inside; data access via
database / repositories as in existing examples
|
Reference implementations: docs/uml/architecture/system_context_view.puml,
container_view.puml, system_component_view.puml.
Titles, labels, and arrows
-
Title line: Use
title Study App - C4 L1 …,L2 …, orL3 …for structural views; usetitle Study App - Sequence: …for interactions. -
Element text: First line = name; optional second line with
\nfor technology or path (for exampleFastAPI · REST, file path, or DB file name). Keep lines short; prefer 2 lines over wrapping clutter. -
Primary dependencies: solid arrows
-->with a short verb or protocol on the arrow (for exampleHTTPS / JSON,SQL). -
Secondary or optional links: dotted arrows
..>(metrics, probes, optional deploy, non-blocking integrations). -
Aliases: Use
as ShortNamefor long labels to keep relationship lines readable. -
Layout hints: Use hidden links (for example
-[hidden]down-,-[hidden]right-) to align ranks or stack layered components; express real dependencies only with-->or..>.
Sequence diagrams
- Start with
autonumberandhide footboxunless there is a reason not to. -
Use stereotypes that match the role:
actor,boundary(API edge),control(or plainparticipant) for services,databasefor persistence. - Use
activate/deactivatefor the main request path; usealt/elsefor branching flows. - Respect shared skin: message alignment, padding, and max message width come from
style.puml—avoid overriding them in individual files unless necessary.
Density, legends, and splitting
- C4 zoom discipline: one diagram answers one zoom level. If a structural view mixes context concerns with internal module names, split it (for example separate “required runtime” from “optional observability”).
-
Heuristic: aim for roughly 8–12 major nodes on L1–L2 context/container
diagrams; if labels overlap or arrows multiply, prefer a second
.pumlover shrinking fonts. - Legends: add a legend only when color or line style encodes meaning not obvious from titles; otherwise describe conventions in prose on the HTML page.
-
When layout fights back: shorten edge labels, use aliases, add hidden alignment links, or
raise
nodesep/ranksepin the shared style for everyone—avoid one-off skinparam in a single file unless that diagram is permanently exceptional.
Canonical visual template (shared skin)
The file docs/uml/include/style.puml is the single source of truth for fonts, corner radius, arrow
weight, and fill colors. scripts/regenerate_docs.py inserts it immediately after
@startuml when sending a diagram to Kroki. Do not duplicate those skinparam lines in each
diagram.
| Role | Setting | Values (hex) |
|---|---|---|
| Page / packages | Package background and border | Background #F1F5F9, border #CBD5E1 |
| Software / API / components | Rectangle (default component) | Fill #F8FAFC, border #2563EB |
| Data stores | Database | Fill #FFF7ED, border #C2410C |
| Infrastructure / deployment | Node | Fill #F8FAFC, border #64748B |
| People | Actor | Fill #ECFDF5, border #047857 |
| Arrows | Generalization / dependency lines | Color #64748B, thickness 1.1 |
- Font:
Arial(available on typical Kroki/Linux renderers), size10for elements; arrow labels useArrowFontSize 9(change only via shared style). - Corners:
roundcorner 8;componentStyle rectangle,packageStyle rectanglefor a clean, consistent look. - Lines:
linetype ortho(orthogonal segments—horizontal and vertical only, readable on docs pages),shadowing falseto reduce visual noise. - Spacing:
nodesep,ranksep, andPaddingare set in shared style for readable separation; change only there unless a diagram is split instead. - Web output:
scripts/regenerate_docs.pyrequests SVG from Kroki (plantuml/svg) so diagrams stay sharp at any zoom;dpiandscalein the skin still influence layout density. - Export width and HTML: Shared skin uses
scale max 960 widthso wide exports are capped without upscaling narrow diagrams (fixedscale N widthwithoutmaxwould blow up text on sparse diagrams). HTML must not stretch SVGs to full column width; rules live indocs/assets/docs.cssunder.sys-diagram__canvas img. Operational guide: sizing, sequence alignment when several diagrams sit together, per-fileParticipantPaddingtuning, and a troubleshooting checklist —docs/uml/README.txt(sections Sizing and visual consistency and When something looks wrong after render).
To render a diagram without the shared skin (rare debugging), put !NO_STYLE on the
line immediately after @startuml; the merge script strips the marker and skips injection.
File layout and outputs
docs/uml/architecture/*.puml— C4-style structural views.docs/uml/sequences/*.puml— sequence diagrams.docs/uml/include/style.puml— shared skin only (not sent to Kroki as a standalone diagram).docs/uml/rendered/*.svg— generated; never edit by hand. Commit SVGs together with source changes.
Regenerate: make docs-fix or .venv/bin/python scripts/regenerate_docs.py. Verify CI:
make uml-check or scripts/regenerate_docs.py --check.
Scope
- In scope: Conventions for PlantUML in this repository, C4-level mapping, shared colors and typography, relationship semantics, sequence defaults, and render pipeline expectations.
- Out of scope: Structurizr DSL, official C4-PlantUML library adoption, and diagramming for non-docs assets—unless a later ADR extends this one.
Alternatives considered
-
C4-PlantUML library
- Pros: explicit C4 stereotypes and legends.
- Cons: typically relies on
!include; Kroki does not expand includes in our single-file upload path without bundling—would require a local preprocessor or checked-in expanded files.
-
Mermaid in Markdown
- Pros: readable in GitHub without render step.
- Cons: second notation and styling path; weaker alignment with existing PlantUML-based docs pipeline.
-
Manual edits to rendered images
- Pros: pixel-perfect control.
- Cons: not reproducible; violates docs-as-code for diagrams.
Consequences
Positive
- One visual language and one render path; onboarding and reviews are simpler.
- C4 thinking stays aligned with industry practice while keeping tooling minimal.
- Shared skin changes propagate to all diagrams on the next regeneration.
Trade-offs
- Authors must learn PlantUML and respect file locations; sequence diagrams are more verbose than Mermaid.
- Large diagrams may need splitting or zoom-level discipline—C4 exists partly to avoid a single giant canvas.
Compatibility and migration
- Backward-compatibility impact: none for runtime systems; documentation-only.
- Migration plan: new or touched diagrams follow this ADR; migrate legacy ad hoc styling when those files are next edited.
- Rollback strategy: revert ADR and skin changes; regenerate SVGs from previous commits.
Implementation plan
- Keep
style.pumlandregenerate_docs.pyin sync; any new global skinparam belongs in the shared file. - When adding a diagram, copy structure from an existing architecture or sequence file; run
make docs-fixand commit sources plus SVGs. - Cross-link this ADR from engineering or system-design docs when those pages are updated.
Validation
- Technical checks:
make uml-checkpasses after changes; Kroki renders without errors. - Documentation: HTML references
docs/uml/rendered/<name>.svgpaths that match generated files.
References
- C4 model: c4model.com.
- PlantUML: plantuml.com.
- Repository:
docs/uml/README.txt(commands, sizing guide, troubleshooting),docs/uml/include/style.puml. - Related ADRs: ADR 0001 (docs as code).
Page history
| Date | Change | Author |
|---|---|---|
| Added Page history section (repository baseline). | Ivan Boyarkin |