Drawing clean architecture diagrams in C4 with PlantUML

puml.online

C4 is a methodology that gives architecture diagrams a fixed storyboard; PlantUML is its best carrier. Here’s the shortest path from zero to productive.

What is C4

C4, from Simon Brown, splits architecture into four zoom levels:

Level View Audience
1 — System Context What value does the system provide? Any stakeholder
2 — Container Which independently-deployable units make it up? Architects / ops
3 — Component What modules live in a container? Developers
4 — Code Class diagram inside a module (optional) Developers

Core idea: no single diagram explains everything; layered zoom handles it.

The toolkit: C4-PlantUML

C4 has an official PlantUML implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

Person(user, "User", "uses browser")
System_Boundary(ecommerce, "E-commerce") {
Container(web, "Web App", "React", "user-facing UI")
Container(api, "API Server", "Go", "REST API")
ContainerDb(db, "Primary DB", "PostgreSQL", "orders / users")
Container(redis, "Cache", "Redis", "hot keys")
}

System_Ext(payment, "Payment Gateway", "third party")

Rel(user, web, "uses")
Rel(web, api, "JSON/HTTPS")
Rel(api, db, "read / write")
Rel(api, redis, "query")
Rel(api, payment, "charge")
@enduml

C4-PlantUML adds on top of PlantUML:

  • Person / System / System_Ext — humans or external systems
  • Container / ContainerDb / ContainerQueue — deployable units
  • Component / ComponentDb — modules inside a container
  • Rel / Rel_Back — relationships
  • System_Boundary / Container_Boundary — boundaries

Internal networks: vendor it

!include of remote URLs often times out behind corporate networks. Vendor it:

1
git clone --depth=1 https://github.com/plantuml-stdlib/C4-PlantUML.git source/plantuml/c4

Then:

1
2
!includeurl https://raw.githubusercontent.com/.../C4_Container.puml  ❌
!include /plantuml/c4/C4_Container.puml ✅

Add a CI step to keep vendored C4 in sync.

Full example: all 4 levels

Level 1 — System Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@startuml
!include /plantuml/c4/C4_Context.puml

LAYOUT_TOP_DOWN()

Person(visitor, "Visitor")
Person(admin, "Operations")

System(blog, "Blog system", "users write + read posts")

System_Ext(cdn, "CDN", "static file delivery")
System_Ext(sso, "SSO", "single sign-on")

Rel(visitor, blog, "browses / writes")
Rel(admin, blog, "manages")
Rel(blog, cdn, "pulls assets")
Rel(blog, sso, "authenticates")
@enduml

Level 2 — Container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@startuml
!include /plantuml/c4/C4_Container.puml

Person(visitor, "Visitor")

System_Boundary(blog, "Blog system") {
Container(web, "Web", "Next.js", "SSR + static export")
Container(api, "API", "Node.js", "REST")
ContainerDb(mysql, "Blog DB", "MySQL", "posts / comments")
ContainerDb(redis, "Cache", "Redis", "view counts / rate limit")
}

System_Ext(cdn, "CDN")
System_Ext(sso, "SSO")

Rel(visitor, web, "https")
Rel(web, api, "JSON")
Rel(api, mysql, "SQL")
Rel(api, redis, "cache read/write")
Rel(web, cdn, "pulls static")
Rel(api, sso, "token verify")
@enduml

Level 3 — Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@startuml
!include /plantuml/c4/C4_Component.puml

Container_Boundary(api, "API Server") {
Component(auth_ctrl, "AuthController", "Express", "login / auth")
Component(article_ctrl, "ArticleController", "Express", "CRUD")
Component(comment_ctrl, "CommentController", "Express", "comments")
Component(article_repo, "ArticleRepo", "TypeORM", "data access")
Component(comment_repo, "CommentRepo", "TypeORM", "data access")
Component(cache, "Cache", "ioredis", "cache adapter")
}

Rel(auth_ctrl, article_repo, "queries users")
Rel(article_ctrl, article_repo, "read/write")
Rel(article_ctrl, cache, "cache read")
Rel(comment_ctrl, comment_repo, "read/write")
Rel(comment_ctrl, cache, "cache read")
@enduml

Level 4 — Code (optional)

Use plain class diagrams:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@startuml
class Article {
+id: int
+title: string
+content: text
+author: User
+comments: Comment[]
}
class User {
+id: int
+name: string
+email: string
}
class Comment {
+id: int
+body: text
+author: User
}
Article "1" *-- "*" Comment : has
Article "*" --> "1" User : author
@enduml

C4 vs off-the-shelf component diagram

Dimension Plain component diagram C4
Zoom levels Single, free 4 fixed levels
Boundaries Free hand-drawn System_Boundary standard
Relationship types Any Rel standardized + labeled
Renderer Native PlantUML C4-PlantUML stdlib
Learning curve Low Medium (4-level concept first)
Use case Quick sketch Long-term team doc

C4’s biggest value: different people draw similar-looking diagrams, which lowers the cost of team communication.

Practical tips

Layout direction

1
2
3
4
LAYOUT_TOP_DOWN()        ! default
LAYOUT_WITH_LEGEND() ! adds legend
LAYOUT_AS_SKETCH() ! hand-drawn style
SHOW_DYNAMIC_LEGEND() ! dynamic legend

Colors and theme

1
2
UpdateElementStyle("c4model:boundary", $bgColor="#FAFAFA", $borderColor="#2F4858")
UpdateRelStyle(RelLabelColor="#2F4858")

Dark background:

1
2
UpdateElementStyle("c4model:boundary", $bgColor="#161B22", $borderColor="#58A6FF")
UpdateElementStyle("c4model:person", $bgColor="#21262D", $fontColor="#E6EDF3")

Multi-zoom in one image

Level 2 with LAYOUT_TOP_DOWN() + LAYOUT_WITH_LEGEND() renders cleanly for multi-zoom slides.

My team’s practice

4 levels, 4 files:

1
2
3
4
5
docs/architecture/
c4-level-1.puml
c4-level-2.puml
c4-level-3.puml
diagrams.puml

CI auto-renders SVG:

1
2
3
4
5
- run: |
curl -L -o plantuml.jar https://github.com/plantuml/plantuml/releases/latest/download/plantuml.jar
find docs/architecture -name '*.puml' | while read f; do
java -jar plantuml.jar -tsvg -nometadata "$f"
done

Each render commits back. New hires see Level 1/2/3 and grok the system in 5 minutes.

Common mistakes

  • Level 1 too detailed — Context should have 3-6 elements. More than that and it’s really a Level 2.
  • Drawing all 4 levels for every system — small systems need only Level 2; only go to Level 3 for complex distributed systems.
  • No labels on relationships — every Rel(A, B) should say what the interaction is.
  • Wrong choice of Container — Container is a “deployable, separate process” unit. One Go binary is a Container; a goroutine inside it is not.
  • Forget to vendor — remote !include often fails on internal networks; CI failure is a good teacher.

Gotchas

  • Latest tag isn’t always default GitHub branch — pin to a v2.x tag.
  • ShowPerson / HidePerson controls person display level — handy on complex diagrams.
  • AddRelTag styles relationships: AddRelTag("async", $lineColor="#888", $lineStyle=DashedLine), then Rel(..., "async") applies it.

When not to use C4

  • Flow / state diagrams — sequence or state, not C4.
  • Marketing deck illustrations — C4 is too clean, lacks visual punch.
  • Diagrams with 1-2 elements — overkill; a single box + arrow is fine.

TL;DR

C4 gives architecture diagrams a fixed grammar. PlantUML is its best carrier. Four zoom levels mean everyone draws similar-looking diagrams — and diagrams that humans can read are diagrams that earn their keep.

  • Title: Drawing clean architecture diagrams in C4 with PlantUML
  • Author: puml.online
  • Created at : 2026-07-29 14:50:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-c4-architecture-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.