= C4Drill image:https://pkg.go.dev/badge/github.com/Djarvur/c4drill.svg["GoDoc",link="https://pkg.go.dev/github.com/Djarvur/c4drill"] image:https://github.com/Djarvur/c4drill/actions/workflows/pr-sanity.yml/badge.svg["Build Status",link="https://github.com/Djarvur/c4drill/actions/workflows/pr-sanity.yml"] image:https://coveralls.io/repos/github/Djarvur/c4drill/badge.svg?branch=master&service=github["Coverage Status",link="https://coveralls.io/github/Djarvur/c4drill?branch=master"]
:toc:
:toc-title: Contents
:icons: font
Describe your architecture once in a TOML file; C4Drill generates a
hierarchy of cross-linked C1/C2/C3 SVG diagrams with automatic drill-down
navigation. The model is the diagram: edit the TOML, re-run the tool,
and every level plus all navigation links is regenerated consistently.
No runtime, no GUI, no manual drawing.
== What It Is and What Problem It Solves
One TOML model produces the C1 context diagram below, the C2 container
diagrams, the C3 component diagrams, and every drill-down link between
them. Open any generated SVG to click through.
image::examples/cloud-system/cloud-system.svg[Cloud System — C1 diagram,width=400]
Real systems do not fit one sheet: dozens to hundreds of elements on a
single diagram are unreadable, and hand-maintaining a linked C1 → C2 →
C3 hierarchy drifts out of sync with the code. C4Drill generates the
hierarchy from the model, so every level and all navigation links stay
consistent after each re-run.
=== Why Not an Existing Tool?
[cols="1,2,2",options="header",]
|===
|Tool |Good at |But
|PlantUML (+ C4 module) |Rich syntax, Markdown integration |No enforced C4 levels; every level is a separate hand-written diagram — the drill-down hierarchy is not generated
|Mermaid |Diagrams straight from Markdown (GitHub, Obsidian) |Flat pictures: no C4 model, no levels, no zoom-in navigation
|Structurizr DSL |Purpose-built C4 modeling language |More general than the task — a modeling platform with workspaces; C4Drill is deliberately minimal and strict
|LikeC4 |The closest: model + nested views with drill-down |Requires a runtime/workspace; C4Drill emits static files you can commit and open anywhere
|===
C4Drill targets the *static-artifact* case: diagrams that live in the
repository, are reviewed in pull requests, and render everywhere.
=== Why TOML?
No new DSL to learn. TOML is a configuration format most engineers
already read, and the schema *is* the C4 model: `type = "container"` and
`name = "API Service"` read like prose. A bespoke DSL (or HCL) would add
syntax to learn without adding modeling power.
=== Is This for You?
* *Yes*, if you want the level hierarchy generated and kept in sync as
versionable static files.
* *Probably not*, if you only need quick throwaway diagrams inside
Markdown — Mermaid or PlantUML will serve you better.
== Install
[source,bash]
----
go install github.com/Djarvur/c4drill/cmd/c4drill@latest
----
Or build from source:
[source,bash]
----
git clone https://github.com/Djarvur/c4drill
cd c4drill
go build -o c4drill ./cmd/c4drill
----
== Quick Start
[source,bash]
----
# Generate SVG diagrams (default)
c4drill architecture.toml
# Generate to specific directory
c4drill architecture.toml -o ./docs/diagrams
# Generate DOT format for customization
c4drill architecture.toml -f dot -o ./output
----
== Examples
The models below are adapted from the
https://github.com/likec4/likec4/tree/main/examples[likec4 examples].
Generated diagrams are committed next to their TOML sources, so what you
see here is exactly what `c4drill` produces. Features likec4 has that
C4Drill does not (dynamic and deployment views, tags, metadata,
view-local styles) are either mapped to the closest C4Drill equivalent
or dropped with a note in the source file.
=== Cloud System
A SaaS platform with two external parties and multi-level drill-down.
image::examples/cloud-system/cloud-system.svg[Cloud System — C1 diagram,width=720]
The C1 diagram already shows one nesting level; clicking the nodes
drills down further:
* link:examples/cloud-system/cloud-system/cloud.svg[Cloud System — C2 containers]
* link:examples/cloud-system/cloud-system/amazon.svg[Amazon — C2 containers]
* link:examples/cloud-system/cloud-system/amazon/rds.svg[RDS — C3 (PostgreSQL)]
* link:examples/cloud-system/cloud-system/amazon/sqs.svg[SQS — C3 (queues)]
* link:examples/cloud-system/cloud-system/amazon/lambdas.svg[Lambdas — C3 (enrichment)]
Regenerate:
[source,bash]
----
c4drill examples/cloud-system/cloud-system.toml
c4drill examples/cloud-system/cloud-system.toml --expanded
----
==== All-Expanded Diagram
`--expanded` renders every unit on a single diagram — no drill-down
navigation, useful for a one-shot review of the whole architecture:
image::examples/cloud-system/cloud-system.expanded.svg[Cloud System — all-expanded diagram,width=720]
=== Vehicle Platform
A stress test: very long display names and channel-technology labels.
C4Drill sizes labels from their content — tune the shape with
`--label-ratio` (default 1.6) or the `C4DRILL_LABEL_RATIO` environment
variable.
image::examples/overflow-test/overflow-test.svg[Vehicle Platform — C1 diagram,width=720]
The longest labels live in the sensor processing pipeline (C3):
image::examples/overflow-test/overflow-test/vehiclePlatform/sensorProcessing.svg[Sensor processing pipeline — C3 diagram,width=720]
Parallel edges between the same pair of units collapse to a single
thicker edge in resolved views (first-wins dedup, D-01); the
`--expanded` diagram shows every parallel link with its own label.
Regenerate:
[source,bash]
----
c4drill examples/overflow-test/overflow-test.toml
----
=== Effect of Rank
A minimal graph showing rank hints. The per-link `rank` attribute puts
the link's endpoints on the same rank (`rank = "equal"`) or orders them
along the layout direction (`rank = "forward"` / `"reverse"`).
`D` carries a link to `C` to satisfy the orphan rule (see
<<Validation Rules>>).
image::examples/rank-for-better-layout/rank-for-better-layout.svg[Effect of Rank — C1 diagram,width=520]
Regenerate:
[source,bash]
----
c4drill examples/rank-for-better-layout/rank-for-better-layout.toml
----
== TOML Format
=== Minimal Example
[source,toml]
----
[properties]
name = "My System"
[user]
type = "person"
name = "User"
[webapp]
type = "system"
name = "Web Application"
# Every unit needs at least one link (orphan rule)
[[webapp.link]]
peer = "user"
description = "Uses"
----
=== Properties Section
The `++[++properties++]++` section defines global settings for your
architecture:
[source,toml]
----
[properties]
name = "E-Commerce Platform" # Required: Architecture name
description = "Online store system" # Optional: Description
color = "#E3F2FD" # Optional: Default background color
style = "filled" # Optional: Default visual style
border = "#1565C0" # Optional: Default border color
edges = "spline" # Optional: Edge routing (straight|spline|square)
lineLength = 40 # Optional: Max line length before wrap (0=auto)
expanded = ["payments"] # Optional: Units to expand by default
----
=== Unit Types
Each unit is defined as a TOML section. The section name becomes the
unit's identifier.
*Type is optional* — defaults based on nesting level:
* Root-level units → `system`
* Units inside system/box → `container`
* Units inside container → `component`
==== Person (Actor)
[source,toml]
----
[user]
type = "person" # Required
name = "Customer" # Optional: defaults to humanized identifier
description = "Online shopper" # Optional
----
==== External Person (External Actor)
An actor outside the organization (partner, regulator, third-party
user). Same fields as `person`:
[source,toml]
----
[auditor]
type = "personExternal"
name = "Compliance Auditor"
description = "External regulatory auditor"
----
==== System
[source,toml]
----
[webapp]
type = "system"
name = "Web Application"
description = "Frontend web app"
technology = "React, TypeScript" # Optional: Shown in diagram
----
==== External System
[source,toml]
----
[stripe]
type = "systemExternal"
name = "Stripe"
description = "Payment processor"
----
==== Database
[source,toml]
----
[postgres]
type = "db"
name = "PostgreSQL"
description = "Primary database"
technology = "PostgreSQL 15"
----
==== External Database
[source,toml]
----
[analytics]
type = "dbExternal"
name = "Analytics DB"
description = "Third-party analytics"
----
==== Queue
[source,toml]
----
[rabbitmq]
type = "queue"
name = "Message Queue"
description = "Async job processing"
technology = "RabbitMQ"
----
==== External Queue
[source,toml]
----
[sqs]
type = "queueExternal"
name = "AWS SQS"
description = "External message queue"
----
==== Box (Grouping Container)
[source,toml]
----
[cloud]
type = "box"
name = "AWS Cloud"
description = "Cloud infrastructure"
----
==== Reference (External Documentation URL)
Any unit accepts an optional `reference` field — an external
documentation URL. When set, a 📖 marker appears next to the unit name
and the node becomes clickable (GraphViz's native `URL` attribute in
SVG). In `-f html` output, external `http(s)` references open in a new
tab, distinct from internal drill-down navigation.
[source,toml]
----
[api]
type = "system"
name = "API Service"
reference = "https://wiki.example.com/api-runbook" # Optional: 📖 marker, clickable
----
An empty string and an omitted field are equivalent (no 📖, not
clickable).
=== Optional Name (Humanization)
The `name` field is *optional*. When omitted, the display name is
derived from the *last segment* of the unit's identifier via a dumb
camelCase split:
[source,toml]
----
# Explicit name (always wins — use this for acronyms or custom labels)
[linuxSystem.localIDP]
name = "My Custom Name"
# Name omitted — humanized from the last path segment "sessionManager"
[linuxSystem.sessionManager]
# displays as "Session Manager"
----
*Humanization rules:*
* Splits camelCase boundaries and Title-cases each word.
* Operates on the *last path segment only* —
`++[++linuxSystem.localIDP++]++` becomes "Local IDP", not "Linux System
Local IDP".
* Examples: `sessionManager` → "Session Manager", `localIDP` → "Local
IDP", `linuxSystem` → "Linux System".
*Acronyms:* acronym preservation is *not* supported — `gRPC` humanizes
to "Grpc". Set `name =` explicitly to override; an explicit `name =`
always wins.
*Backward compatibility:* models that set `name =` on every unit are
unaffected — humanization only fires when `name` is omitted.
=== Optional Type (Inference)
The `type` field is *optional*. When omitted, the type is inferred from
the parent unit's type (and, for the generic `db`/`queue` types,
promoted to the level-specific variant based on nesting). Two rules
apply:
*1. Default type by parent* — the type assigned when `type` is omitted
entirely:
[cols=",,",options="header",]
|===
|Parent type |Inferred child type |Level
|(none — root) |`system` |C1
|`system` |`container` |C2
|`box` |`system` |C1 (same-level grouping)
|`container` |`component` |C3
|`containerBox` |`container` |C2 (same-level grouping)
|`componentBox` |`component` |C3 (same-level grouping)
|(other: db, queue, etc.) |`system` |C1 fallback
|===
*2. Generic `db`/`queue` promotion* — when `type = "db"` or
`type = "queue"` is set explicitly, the type is promoted to the
level-specific variant based on the parent:
[cols=",,,",options="header",]
|===
|Parent type |`db` becomes |`queue` becomes |Level
|(none) or `box` |`db` |`queue` |C1 (unchanged)
|`system` or `containerBox` |`containerDb` |`containerQueue` |C2
|`container` or `componentBox` |`componentDb` |`componentQueue` |C3
|===
*Before/after example:*
[source,toml]
----
# BEFORE — explicit types (verbose)
[platform]
type = "system"
[platform.webapp]
type = "container"
[platform.webapp.cache]
type = "componentDb" # generic db promoted because parent is container
# AFTER — type omitted, inferred (identical result)
[platform]
# type omitted → inferred "system" (no parent)
[platform.webapp]
# type omitted → inferred "container" (parent is system)
[platform.webapp.cache]
type = "db"
# explicit generic db → promoted to "componentDb" (parent is container)
----
An explicit non-generic `type =` always wins (no inference runs).
Source: `defaultTypeForParent` and `inferGenericType` in
`internal/parser/parser.go`.
=== Nesting (C2/C3 Diagrams)
Systems and boxes can contain subunits using dotted notation:
[source,toml]
----
[mainapp] # C1 level
type = "system"
name = "Main Application"
[mainapp.api] # C2 level (container)
type = "container"
name = "API Service"
[mainapp.webapp] # C2 level (container)
type = "container"
name = "Web App"
[mainapp.api.handlers] # C3 level (component)
type = "component"
name = "HTTP Handlers"
[mainapp.api.services] # C3 level (component)
type = "component"
name = "Business Services"
----
=== Links (Relationships)
Define relationships between units using `[[link]]` (outgoing) or
`[[linkFrom]]` (incoming). Define each relationship exactly once — the
two forms are just different places to write the same edge:
[source,toml]
----
[user]
type = "person"
name = "User"
[webapp]
type = "system"
name = "Web Application"
# Outgoing link: User → Webapp (defined on the source)
[[webapp.link]]
peer = "user"
technology = "HTTPS"
description = "Browses"
[api]
type = "system"
name = "API Service"
# Incoming link: Webapp → API (defined on the target)
[[api.linkFrom]]
peer = "webapp"
technology = "REST/JSON"
description = "Calls"
----
==== Link Attributes
[cols=",,",options="header",]
|===
|Attribute |Description |Example
|`peer` |Target unit identifier (required) |`"user"`, `"platform.api"`
|`technology` |Protocol/technology label |`"HTTPS"`, `"gRPC"`, `"TCP"`
|`description` |Relationship description |`"Sends events to"`
|`arrow` |Arrow direction |`"forward"`, `"reverse"`, `"bidirectional"`, `"none"`
|`rank` |Layout ranking hint |`"forward"`, `"reverse"`, `"equal"`
|`color` |Edge color |`"blue"`, `"++#++FF5733"`
|`style` |Line style |`"solid"`, `"dashed"`, `"dotted"`
|`labelPosition` |Where the label appears |`"middle"`, `"tail"`, `"head"`
|===
==== Multiple Links
[source,toml]
----
[api]
type = "system"
name = "API"
[[api.link]]
peer = "user"
technology = "HTTPS"
description = "Authenticates"
[[api.link]]
peer = "webapp"
technology = "REST"
description = "Serves data"
----
*Parallel edges:* when several links connect the same pair of units,
resolved views collapse them into a single thicker edge (the first one
wins); the `--expanded` diagram shows every parallel link separately.
=== Expanded Units (Drill-Down)
Mark units as "expanded" to generate C2/C3 diagrams for them:
[source,toml]
----
[properties]
name = "My System"
expanded = ["mainapp"] # Generate C2 for mainapp
[mainapp]
type = "system"
name = "Main Application"
expanded = ["mainapp.api"] # Generate C3 for mainapp.api
[mainapp.api]
type = "container"
name = "API Service"
[mainapp.api.handlers]
type = "component"
name = "Handlers"
[mainapp.api.services]
type = "component"
name = "Services"
[[mainapp.api.handlers.link]]
peer = "mainapp.api.services"
description = "Calls"
----
*Output structure:*
[source,text]
----
output/
├── architecture.svg # C1 diagram
├── architecture/ # C2 diagrams directory
│ └── mainapp.svg # C2 for mainapp
│ └── mainapp/ # C3 diagrams directory
│ └── api.svg # C3 for mainapp.api
----
=== Styling
Override default colors and styles per unit:
[source,toml]
----
[webapp]
type = "system"
name = "Web Application"
color = "#4A90D9" # Background color
border = "#2E5A8B" # Border color
style = "solid" # Border style (solid|dashed|dotted)
edges = "spline" # Edge routing for this unit's links
width = 300 # Explicit label width (0=auto)
height = 200 # Explicit label height (0=auto)
----
`width` and `height` are rarely needed — labels are normally sized
from their content (see `--label-ratio` in the CLI Reference).
=== Templates
A template is a `++[++template.++<++name++>++]++` table that declares
parameters and a unit shape (including subunits and links); each
`++[[++use++]]++` directive instantiates it with concrete values.
[source,toml]
----
[template.microservice]
params = ["name", "tech", "upstreamBus"]
name = "${name} Service"
type = "container"
technology = "${tech}"
description = "${name} handles its domain"
reference = "https://wiki.example.com/${name}"
[[template.microservice.link]]
peer = "${upstreamBus}"
description = "Publishes ${name} domain events"
[[use]]
template = "microservice"
parent = "platform"
name = "auth"
tech = "Go, gRPC"
upstreamBus = "messageBus"
----
*Rules:*
* All declared params are *required* on every `++[[++use++]]++` (no
defaults); a missing param is a hard error.
* `$++{++param}` substitutes into every string field — name,
description, technology, reference, color, and link fields (peer,
description, technology).
* The link set is *fixed*: a template with one
`++[[++template.X.link++]]++` produces exactly one link per
instantiation (no fan-out / `for++_++each`).
* Subunit subtrees are supported (declare `++[++template.X.child++]++`);
the subunit key is verbatim, only field values are substituted.
* Duplicate unit paths across instantiations are a hard error.
See `skill/examples/06-templates.toml` for a runnable example.
=== Multi-File Composition (Include)
Assemble a diagram from multiple TOML files. Each `++[[++include++]]++`
directive pulls in another file relative to the including file's
directory and merges its units into the model.
[source,toml]
----
# entry.toml
[platform]
type = "system"
name = "Platform"
[[include]]
path = "auth.toml"
[[include]]
path = "templates.toml"
once = true
----
*Rules:*
* Paths are *relative to the including file's directory* (not the CLI
cwd).
* Includes are *transitive* (an included file may itself include
others).
* `once = true` deduplicates by canonical path — a file included again
(even via a different path) is skipped.
* The merge is *flat* (no namespacing): included units append in include
order. Cross-file subunits are supported — an included file may
re-declare a parent declared in the entry and contribute subunits under
it.
* Include cycles are a fatal error; missing files are a hard error.
* Properties follow root-file-wins (the entry's `++[++properties++]++`
takes precedence).
See `skill/examples/08-include/` for a runnable multi-file example.
=== Relative Peer Resolution
A bare `peer` value (no dot) resolves against the enclosing parent's
ancestor scopes — walking up nearest-first until a sibling match is
found. A peer *with a dot* is absolute and used as-is.
[source,toml]
----
[platform.api]
# Bare peer "cache" resolves via walk-up:
[[platform.api.link]]
peer = "cache" # sibling: platform.cache (nearest ancestor scope match)
[[platform.api.link]]
peer = "platform.cache" # absolute: has a dot, used as-is
----
*The four resolution cases:*
* *Sibling match* — the nearest ancestor scope (the immediate parent)
has a child with that name.
* *Aunt/grandparent match* — walk up past the parent; a grandparent's
child matches.
* *Root match* — walk all the way to the top-level scope.
* *Absolute fallback* — a peer containing a dot is never walked-up; it
is used verbatim.
Multiple matches at the same depth are impossible (sibling keys are
unique per parent). A miss at root is a hard error naming the peer and
the host unit.
See `skill/examples/07-relative-peer.toml` for a runnable example
demonstrating all four cases.
== Full Example
[source,toml]
----
[properties]
name = "E-Commerce Platform"
description = "Online shopping system"
edges = "spline"
expanded = ["webapp", "webapp.api"]
# External actors
[customer]
type = "person"
name = "Customer"
description = "Online shopper"
[admin]
type = "person"
name = "Admin"
description = "System administrator"
# Main system with containers
[webapp]
type = "system"
name = "Web Application"
description = "Main e-commerce platform"
technology = "Go, React"
expanded = ["webapp.api"]
[webapp.frontend]
type = "container"
name = "Frontend"
description = "React web application"
technology = "React, TypeScript"
[webapp.api]
type = "container"
name = "API Service"
description = "Backend REST API"
technology = "Go"
expanded = ["webapp.api"]
[webapp.api.handlers]
type = "component"
name = "HTTP Handlers"
description = "Request routing"
[webapp.api.services]
type = "component"
name = "Business Logic"
description = "Core services"
[webapp.db]
type = "db"
name = "PostgreSQL"
description = "Primary database"
technology = "PostgreSQL 15"
# generic `db` is promoted to `containerDb` by the nesting level
# External dependencies
[stripe]
type = "systemExternal"
name = "Stripe"
description = "Payment processing"
[redis]
type = "queue"
name = "Redis"
description = "Session cache"
technology = "Redis"
# Relationships (every unit needs at least one — orphan rule)
[[customer.link]]
peer = "webapp.frontend"
technology = "HTTPS"
description = "Shops"
[[admin.link]]
peer = "webapp.frontend"
technology = "HTTPS"
description = "Administers"
[[webapp.frontend.link]]
peer = "webapp.api.handlers"
technology = "REST/JSON"
description = "Calls"
[[webapp.api.handlers.link]]
peer = "webapp.api.services"
description = "Dispatches"
[[webapp.api.services.link]]
peer = "webapp.db"
technology = "SQL"
description = "Persists"
[[stripe.linkFrom]]
peer = "webapp.api.services"
technology = "Stripe API"
description = "Processes payments"
[[redis.linkFrom]]
peer = "webapp.api.services"
technology = "TCP"
description = "Caches sessions"
----
== CLI Reference
[source,text]
----
c4drill <input.toml> [flags]
Flags:
--expanded Generate all-expanded diagram showing all units
-f, --format string Output format (dot|svg|html) (default "svg")
-h, --help help for c4drill
--label-ratio float Width:height ratio for unit labels (default: 1.6, credit card proportions)
-o, --output string Output directory (default: same as input file)
-v, --version version for c4drill
----
* `--expanded` renders a single diagram containing *all* units (every
level), instead of the per-level drill-down set. The file is named
`++{basename}++.expanded.++{format}++` and contains no drill-down
links.
* `--label-ratio` overrides the width:height ratio used to size unit
labels (default 1.6 — credit-card proportions). Higher values produce
wider, shorter labels; lower values produce narrower, taller ones.
The same value can be set via the `C4DRILL_LABEL_RATIO` environment
variable; the flag takes precedence.
=== Output Format
* *svg* (default): Rendered SVG diagrams with clickable navigation links
* *html*: Self-contained HTML files (SVG inlined) with working
navigation in Safari/WebKit, which silently ignores SVG `++<++a++>++`
hyperlinks. Use `-f html` when diagrams will be opened in Safari or
viewed via `file://`.
* *dot*: Raw GraphViz DOT format for customization
=== Exit Codes
* *0*: Success (silent output)
* *1*: Error (parse failure, validation error, I/O error)
Errors are written to stderr, making the tool suitable for scripting.
== Validation Rules
[arabic]
. *Referenced units must exist* - Links can only reference defined units
. *No links on containers* - Units with subunits cannot have their own
links (`link` or `linkFrom`)
. *No linking to containers* - Cannot link to units that have subunits
. *Subunits only for grouping types* - Only `system`, `box`,
`container`, `containerBox`, and `componentBox` can contain subunits
(leaf types such as `db`, `queue`, and the level-specific variants
cannot)
. *Nesting hierarchy* - Top-level units must be C1 types; inside a
`system` — C2 types (containers); inside a `container` — C3 types
(components); boxes group same-level units only
. *No orphans* - Every unit must have at least one incoming or outgoing
link, or contain subunits
== Architecture
C4Drill implements a compiler-style pipeline:
[source,text]
----
TOML → Parse → Validate → Generate Views → Build Graphs → Render → Write Files
----
* *Parser*: Reads TOML into structured model
* *Validator*: Enforces C4 rules and reference integrity
* *View Generator*: Creates C1/C2/C3 views from model
* *Graph Builder*: Constructs graphviz-compatible structures
* *Renderer*: Outputs DOT, SVG, or HTML via go-graphviz
* *Writer*: Creates output directory hierarchy
== License
MIT