| name | c4-diagrams |
| description | Covers creating C4 architecture diagrams using PlantUML. Use when documenting system architecture, component relationships, or container layouts for technical documentation. |
C4 Diagrams with PlantUML
This skill covers creating C4 model architecture diagrams using the PlantUML C4 plugin.
Overview
The C4 model provides four levels of abstraction for documenting software architecture:
| Level | Name | Purpose |
|---|
| 1 | Context | System in its environment with users and external systems |
| 2 | Container | High-level technology choices and responsibilities |
| 3 | Component | Components within a container |
| 4 | Code | Class/entity level (rarely needed) |
PlantUML C4 Setup
Include the C4 library at the start of your diagram:
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
' Your diagram here
@enduml
Available includes based on diagram level:
| Include | Use For |
|---|
C4_Context.puml | Level 1 - System Context diagrams |
C4_Container.puml | Level 2 - Container diagrams |
C4_Component.puml | Level 3 - Component diagrams |
C4_Dynamic.puml | Sequence-style interaction diagrams |
C4_Deployment.puml | Infrastructure and deployment diagrams |
Key Files
| File | Purpose |
|---|
scripts/generate-diagrams.sh | Generate PNGs from .puml files using Docker |
context-diagrams.md | System context diagram patterns |
container-diagrams.md | Container diagram patterns |
component-diagrams.md | Component diagram patterns |
Core Elements
People and Systems
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
Person(user, "Platform User", "A user of the Datum Cloud platform")
Person_Ext(external, "External User", "External system user")
System(platform, "Datum Cloud Platform", "Multi-tenant Kubernetes platform")
System_Ext(github, "GitHub", "Source code repository")
Rel(user, platform, "Uses", "HTTPS")
Rel(platform, github, "Pulls code from", "HTTPS")
@enduml
Containers
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
Person(user, "Platform User")
System_Boundary(platform, "Datum Cloud Platform") {
Container(api, "API Server", "Go", "Handles API requests")
Container(controller, "Controller", "Go", "Reconciles resources")
ContainerDb(db, "Database", "PostgreSQL", "Stores platform data")
}
Rel(user, api, "Uses", "HTTPS")
Rel(api, db, "Reads/Writes", "SQL")
Rel(controller, api, "Watches", "Kubernetes API")
@enduml
Components
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
Container_Boundary(api, "API Server") {
Component(handler, "Request Handler", "Go", "Processes HTTP requests")
Component(storage, "Storage Backend", "Go", "Manages persistence")
Component(auth, "Auth Middleware", "Go", "Validates tokens")
}
Rel(handler, auth, "Uses")
Rel(handler, storage, "Uses")
@enduml
Element Reference
People
| Macro | Description |
|---|
Person(alias, label, description) | Internal user |
Person_Ext(alias, label, description) | External user |
Systems
| Macro | Description |
|---|
System(alias, label, description) | Internal system |
System_Ext(alias, label, description) | External system |
System_Boundary(alias, label) | Grouping boundary |
Containers
| Macro | Description |
|---|
Container(alias, label, technology, description) | Generic container |
ContainerDb(alias, label, technology, description) | Database container |
ContainerQueue(alias, label, technology, description) | Message queue |
Container_Boundary(alias, label) | Container grouping |
Components
| Macro | Description |
|---|
Component(alias, label, technology, description) | Generic component |
ComponentDb(alias, label, technology, description) | Database component |
ComponentQueue(alias, label, technology, description) | Queue component |
Internal vs External Elements
Use _Ext variants to distinguish elements your service owns from those it interacts with.
Ownership Rule: Use internal (Container, System) for elements your team owns and
maintains. Use external (Container_Ext, System_Ext) for elements owned by other teams
or external services.
| Element Type | Internal (you own) | External (others own) |
|---|
| System | System() | System_Ext() |
| Container | Container() | Container_Ext() |
| Component | Component() | Component_Ext() |
| Database | ContainerDb() | ContainerDb_Ext() |
Common patterns:
| Scenario | Classification | Rationale |
|---|
| CRD your service defines | Container | You own the resource definition |
| Controller your service runs | Container | You own the code |
| Resource you create, other service processes | Container_Ext | Other team owns the controller |
| Platform service (Gateway, Workload) | Container_Ext | Other team owns |
| Consumer's application | Container_Ext | Consumer owns |
| External SaaS (GitHub, Stripe) | System_Ext | Truly external |
Example: Functions service perspective
' Internal - Functions owns these
Container(function, "Function", "CRD", "We define this resource")
Container(controller, "Function Controller", "Go", "We own this code")
' External - we create but others process
Container_Ext(workload, "Workload", "CRD", "We create, Workload team processes")
Container_Ext(service_pub, "ServicePublication", "Service Connect", "We create, Service Connect processes")
' External - other teams own entirely
Container_Ext(gateway, "Gateway", "Gateway API", "Networking team owns")
Container_Ext(instance, "Instance", "Unikraft", "Workload controller creates")
Context vs Container diagrams:
- Context (Level 1): Use
System for other services in your organization,
System_Ext for truly external services (SaaS, third-party APIs)
- Container (Level 2): Use
Container_Ext for any container not owned by your team,
even if it's within your organization
Relationships
| Macro | Description |
|---|
Rel(from, to, label) | Basic relationship |
Rel(from, to, label, technology) | Relationship with tech |
Rel_D(from, to, label) | Downward relationship |
Rel_U(from, to, label) | Upward relationship |
Rel_L(from, to, label) | Leftward relationship |
Rel_R(from, to, label) | Rightward relationship |
Layout Control
Control diagram layout with directional relationships and layout hints:
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
LAYOUT_WITH_LEGEND()
LAYOUT_LEFT_RIGHT()
' Elements will flow left to right
Container(a, "Service A", "Go", "First service")
Container(b, "Service B", "Go", "Second service")
Container(c, "Service C", "Go", "Third service")
Rel_R(a, b, "Calls")
Rel_R(b, c, "Calls")
@enduml
Layout macros:
| Macro | Effect |
|---|
LAYOUT_TOP_DOWN() | Vertical layout (default) |
LAYOUT_LEFT_RIGHT() | Horizontal layout |
LAYOUT_WITH_LEGEND() | Add legend to diagram |
LAYOUT_AS_SKETCH() | Hand-drawn style |
Multi-Boundary Layout Patterns
When creating diagrams with multiple system boundaries (e.g., cross-control-plane
architectures), use these patterns for clean, readable layouts.
Side-by-Side Boundaries
Place related boundaries horizontally using Rel_L / Rel_R for cross-boundary
relationships:
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
LAYOUT_WITH_LEGEND()
System_Boundary(consumer, "Consumer Control Plane") {
Container(function, "Function", "CRD", "User-facing resource")
Container(endpoint, "ServiceEndpoint", "Service Connect", "Private IP")
}
System_Boundary(provider, "Service Control Plane") {
Container(controller, "Controller", "Go", "Watches resources")
Container(publication, "ServicePublication", "Service Connect", "Exposes service")
}
' Cross-boundary: use Rel_L to connect right boundary to left
Rel_L(controller, function, "Watches", "direct API")
Rel_L(publication, endpoint, "Connected via", "Service Connect")
@enduml
Grid Structure Within Boundaries
Arrange elements in a 2x2 grid using mixed directions:
TopLeft ──Rel_R──→ TopRight
│ │
Rel_D Rel_D
↓ ↓
BottomLeft ←─Rel_L── BottomRight
Example:
' Within a boundary, create grid with mixed directions
Rel_R(httproute, function, "References", "backendRef") ' top row horizontal
Rel_U(gateway, httproute, "Attaches", "parentRef") ' left column vertical
Rel_D(function, endpoint, "Resolves to", "status.endpoint") ' right column vertical
Relationship Direction Guidelines
| Relationship Type | Direction | Rationale |
|---|
| Cross-boundary | Rel_L / Rel_R | Creates side-by-side boundary layout |
| Within-boundary horizontal | Rel_R | Maintains left-to-right data flow |
| Within-boundary vertical | Rel_D / Rel_U | Creates grid rows within boundary |
| Reverse flow (e.g., load balancing) | Rel_L | Shows return path or reverse relationship |
Data Flow Alignment
Arrange elements by data flow direction (left-to-right):
| Position | Element Types |
|---|
| Left | Entry points (Gateway, API endpoints) |
| Middle | Processing/routing (Controllers, Functions) |
| Right | Execution/storage (Instances, Databases) |
Complete Example: Cross-Control-Plane Architecture
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
LAYOUT_WITH_LEGEND()
title Cross-Control-Plane Architecture
System_Boundary(consumer, "Consumer Control Plane") {
Container(httproute, "HTTPRoute", "Gateway API", "Routes traffic")
Container(function, "Function", "CRD", "User-facing resource")
Container(gateway, "Gateway", "Gateway API", "Accepts traffic")
Container(endpoint, "ServiceEndpoint", "Service Connect", "Private IP")
}
System_Boundary(provider, "Service Control Plane") {
Container(controller, "Controller", "Go", "Reconciles resources")
Container(workload, "Workload", "CRD", "Manages instances")
Container(publication, "ServicePublication", "Service Connect", "Exposes workload")
Container(instance, "Instance", "Runtime", "Execution environment")
}
' Consumer internal: grid layout
Rel_U(gateway, httproute, "Attaches", "parentRef")
Rel_R(httproute, function, "References", "backendRef")
Rel_D(function, endpoint, "Resolves to", "status.endpoint")
' Provider internal: grid layout
Rel_R(controller, workload, "Generates", "owner ref")
Rel_D(controller, publication, "Creates")
Rel_D(workload, instance, "Manages", "scale 0-N")
Rel_L(publication, workload, "Load balances to")
' Cross-boundary: horizontal connections
Rel_L(controller, function, "Watches & updates", "direct API")
Rel_L(publication, endpoint, "Connected via", "Service Connect")
@enduml
Tips for Clean Multi-Boundary Layouts
- Consistent boundary placement — Keep related boundaries at same level
(side-by-side or stacked)
- Align cross-boundary connections — Connect elements at similar vertical
positions
- Use grids within boundaries — 2x2 or 2x3 grids keep elements organized
- Minimize line crossings — Adjust directions to avoid overlapping
relationship lines
- Group by function — Entry points together, processing together, execution
together
Styling
Customize colors and appearance:
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
' Custom colors
AddElementTag("critical", $bgColor="#ff0000", $fontColor="#ffffff")
AddElementTag("deprecated", $bgColor="#888888")
Container(api, "API Server", "Go", "Critical service", $tags="critical")
Container(legacy, "Legacy Service", "Java", "Being replaced", $tags="deprecated")
@enduml
Best Practices
Diagram Guidelines
- One purpose per diagram — Don't mix abstraction levels
- Limit elements — 5-10 elements per diagram for readability
- Consistent naming — Use the same names across all diagrams
- Clear descriptions — Each element should have a meaningful description
- Show key relationships — Don't show every possible connection
Documentation Integration
Important: GitHub does not render PlantUML or C4 diagrams directly. You must:
- Create
.puml source files
- Generate PNG images from them
- Embed the PNGs in markdown
Place diagram sources and generated images in docs/architecture/:
docs/
└── architecture/
├── context.puml # Level 1 source
├── context.png # Generated image
├── containers.puml # Level 2 source
├── containers.png # Generated image
└── components/
├── api-server.puml # Level 3 source
├── api-server.png # Generated image
└── controller.puml
└── controller.png
Rendering PNGs
Use the provided script to generate PNG images from PlantUML sources. The script uses Docker, so no local PlantUML installation is required.
./skills/c4-diagrams/scripts/generate-diagrams.sh
./skills/c4-diagrams/scripts/generate-diagrams.sh docs/diagrams
docker run --rm -v "$(pwd)/docs/architecture:/data" plantuml/plantuml -tpng "/data/*.puml"
Always regenerate PNGs when updating .puml files. Commit both the source and generated images.
Embedding in Markdown
Reference the generated PNG in your markdown documentation:
## System Context
The following diagram shows the system context for Datum Cloud Platform.

## Container View

Use relative paths from the markdown file to the PNG. Always include alt text describing the diagram.
Example: Datum Platform Context
@startuml C4_Context
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
LAYOUT_WITH_LEGEND()
title Datum Cloud Platform - System Context
Person(admin, "Platform Admin", "Manages platform configuration")
Person(developer, "Developer", "Deploys and manages workloads")
System(platform, "Datum Cloud Platform", "Multi-tenant Kubernetes platform for edge workloads")
System_Ext(github, "GitHub", "Source code and CI/CD")
System_Ext(cloud, "Cloud Providers", "GCP, AWS infrastructure")
System_Ext(monitoring, "Monitoring", "Observability stack")
Rel(admin, platform, "Configures", "CLI/API")
Rel(developer, platform, "Deploys to", "kubectl/API")
Rel(platform, github, "Pulls from", "HTTPS")
Rel(platform, cloud, "Provisions on", "Cloud APIs")
Rel(platform, monitoring, "Exports to", "OTLP")
@enduml
Anti-patterns to Avoid
- Embedding PlantUML directly in markdown — GitHub won't render it; generate PNGs instead
- Committing .puml without .png — Always generate and commit both source and image
- Too much detail — Context diagrams shouldn't show internal components
- Missing descriptions — Every element needs context
- Inconsistent abstraction — Don't mix containers and components
- Overcrowded diagrams — Split into multiple focused diagrams
- No legend — Always include
LAYOUT_WITH_LEGEND() for clarity