A picture with boxes and arrows is often the fastest way to explain a system. And often the least reliable: The box is called “API”, and you can't tell whether that's a program, an endpoint or a library. The arrow points in one direction, but whether it means “calls”, “sends data” or “depends on”, nobody says. And the picture comes from a presentation program, sits on a drive and shows the system as it was two years ago.
This lesson solves all three problems. The C4 model gives the boxes and arrows fixed meanings and arranges the pictures in levels,
like a map. Mermaid turns text into pictures that GitHub draws, so that a diagram is changed and reviewed in the same pull request as the code.
At the end, docs/c4-model.md is in the Kladde, with a context diagram and a container diagram.
Four levels like a map
The C4 model comes from Simon Brown and is described on c4model.com. Its name comes from the four levels on which it shows a software system. As with a map where you zoom in from the country to the city to the street, each level shows a section of the previous one in more detail:
| Level | Shows | For whom |
|---|---|---|
| 1 Context | the system as one box, the people who use it and the other systems it talks to | everyone, including business people without technical background |
| 2 Containers | the parts that run on their own or hold data, and how they talk to each other | development, operations, security |
| 3 Components | the larger building blocks inside a container | whoever works on this container |
| 4 Code | classes and interfaces of a component | rarely drawn; the development environment shows that |
Four terms with a fixed meaning come with it:
- Person: a human or a role that uses the system, such as “customer” or “the office's team”.
- Software system: what delivers value as a whole and belongs to one team. The Kladde is a software system, the EU's TED interface is another.
- Container: a part of the system that runs on its own or holds data: a web application, a worker, a job, a database, a store for files, a queue, an app in the browser. The term does not mean a Docker container. A container in the sense of C4 can run in a Docker container, but doesn't have to.
- Component: a group of related functions inside a container behind a clear interface, such as “appointment booking” or “outbox” in the web application. A component doesn't run on its own.
For work on the architecture, levels 1 and 2 are the most important. They change rarely enough that you can maintain them, and they answer the questions that come up with almost every decision: Who talks to whom? Where does which data live? What happens when a part fails? You draw level 3 for the containers where a lot is going on right now; level 4 almost never.
The website c4model.com with its examples is under the CC BY 4.0 license. This lesson takes only the terms from there; the diagrams here are our own.
Supplementary diagrams
Besides the four levels, the C4 model knows further diagrams that you draw when needed:
- Dynamic diagram: a particular flow as a sequence of steps, such as “book an appointment”. It shows the same elements as the container diagram, but only those involved in this flow, and numbers the arrows. Works well as a sequence diagram.
- Deployment diagram: where the containers run: which resources in which region, how many replicas, which environment. Azure services appear here with their names.
- System landscape: several software systems of an organization side by side. For the Kladde with its one system, you don't need it.
Microsoft's guide on architecture diagrams calls the same idea layers instead of clutter: A context diagram leads to the container diagram, which leads to a more detailed picture for an important flow. Whoever packs everything into one picture draws a picture that nobody reads.
Rules for every picture
Whether a diagram helps or confuses depends on a few rules. They come from the C4 model and agree with Microsoft's guide:
- A title that names kind and scope: “Kladde, containers” instead of “Architecture”.
- Every element has a name, a kind and a short description. For containers and components, the technology is added: “Web application [Container: ASP.NET Core, Blazor] pages, booking, sign-in”.
- Every arrow has a direction and a label. The label says what flows and, in square brackets, how: “sends confirmation mail [HTTPS, Microsoft Graph]”. An unlabeled arrow says only that something is somehow related to something.
- Arrows point from the initiator to the target. The worker picks up messages from the queue, so the arrow points from the worker to the queue, even though the data flows the other way. The label (“picks up references”) makes the data flow clear. Microsoft's guide explicitly advises against double-headed arrows: If there are two directions, draw two arrows.
- A legend if shapes or lines mean something. If dashed means “later, via a queue”, that goes under the picture.
- Don't distinguish by color alone. A picture has to be readable without color and for people with color vision deficiency; add shape and label.
Diagrams as text with Mermaid
Mermaid is a language in which you write diagrams as text. GitHub draws a code block with the language mermaid in every
Markdown file as a picture, in files, issues and pull requests. That has three advantages over a drawn picture:
- The diagram sits next to the code. Whoever adds a container also changes
docs/c4-model.mdin the same pull request. - Changes are readable. In the diff, “arrow from worker to database added” appears as one new line, not as “image changed”.
- No tool, no license, no drive. A text editor is enough. In Visual Studio Code, an extension for Mermaid shows the preview without you having to push.
Mermaid has a notation of its own for C4 (C4Context, C4Container). Its automatic layout can hardly be influenced, though, and bigger pictures
quickly become confusing. This lesson therefore uses the ordinary flowchart (flowchart)
and writes the C4 meaning into the labels. The rules above apply unchanged.
An example, deliberately not from the Kladde so that the exercises below stay your own: the pre-ordering of a fictional bakery. Customers order bread in the evening for the next morning, and the salesperson prints a packing list early.
---
title: Pre-orders at Krume Bakery, context
---
flowchart TB
customer(["Customer<br/>[Person]<br/>orders bread for the next morning"])
seller(["Salesperson<br/>[Person]<br/>packs the orders in the morning"])
system["Pre-order<br/>[Software system]<br/>takes orders and creates the packing list"]
mail["Mail service<br/>[Software system, external]<br/>delivers confirmations"]
customer -- "orders<br/>[HTTPS, browser]" --> system
seller -- "prints the packing list<br/>[HTTPS, browser]" --> system
system -- "sends the confirmation<br/>[SMTP]" --> mail
mail -- "delivers<br/>[email]" --> customer
What happens here:
- The block between the two
---at the start sets the picture's title. flowchart TBdraws from top to bottom;LRwould be from left to right.- Every line like
customer([…])creates a node: on the left the identifier you need for the arrows, in parentheses the text. The shape comes from the brackets:([…])is a rounded box, here for people;[…]is a rectangle for systems and containers. <br/>breaks the line in the box. That puts the name at the top, the kind in square brackets below it, and then the description.customer -- "orders…" --> systemis an arrow with a label. If the text is in quotation marks, it may contain special characters like parentheses.
For container diagrams, you need three more things:
---
title: Pre-orders at Krume Bakery, containers
---
flowchart TB
customer(["Customer<br/>[Person]"])
subgraph system["Pre-order [Software system]"]
web["Order page<br/>[Container: ASP.NET Core]<br/>form, confirmation"]
db[("Orders<br/>[Container: Azure SQL Database]<br/>orders and outbox")]
queue[["Queue mails<br/>[Container: Service Bus]"]]
sender["Mail sender<br/>[Container: .NET worker]<br/>sends confirmations"]
end
mail["Mail service<br/>[Software system, external]"]
customer -- "orders<br/>[HTTPS]" --> web
web -- "stores order<br/>[TDS]" --> db
web -. "puts a job for the mail in the queue<br/>[AMQP]" .-> queue
sender -. "picks up jobs<br/>[AMQP]" .-> queue
sender -- "sends mail<br/>[SMTP]" --> mail
subgraph … enddraws a frame: the system boundary. What is inside belongs to the team; what is outside doesn't.[(…)]is a cylinder, the usual shape for data stores,[[…]]a box with double sides, here for queues.-. "…" .->is a dashed arrow. In this picture, dashed means: The path goes through a queue, the answer comes later. That goes in the legend under the picture:
Legend: rounded = person, rectangle = system or container, cylinder = data store,
double border = queue; solid = call with response, dashed = via a queue, later.
A word on the technology in the labels: TDS (Tabular Data Stream) is the protocol programs use to talk to SQL Server, AMQP the one of Service Bus. Whoever doesn't know them reads “[TDS]” as “to the database” anyway; whoever does know them immediately knows which ports and libraries are involved.
What a diagram leaves out
Every diagram leaves something out, otherwise it wouldn't be one. What matters is that it happens deliberately and that the reader is told. Typical omissions in the container diagram:
- Cross-cutting services: Application Insights receives telemetry from every container. If you draw that, arrows go from every box to it, and the picture says nothing any more. One sentence under the picture is enough.
- Key Vault and identities: important for security and deployment, but usually noise for the question “who talks to whom about which data”. They belong in the deployment diagram and, from module 4.8, in the threat model.
- The pipeline: It delivers, it doesn't run along.
- Resources, regions, replicas: That is deployment, not containers.
Write a line “Not shown: …” under every picture. That isn't pedantry: Without this line, a reader concludes from the missing Key Vault that there isn't one.
Conversely, a diagram must not be imprecise to look simpler. Microsoft's guide gives the example of a service that is drawn into a subnet although it is reached only through a private endpoint. A picture like that misleads during building and in the security review. Leave things out, yes; show something false, no.
Flows and deployment
A dynamic diagram shows a flow. In Mermaid you write it as a sequence diagram. This is how a booking runs in the Kladde, as you built it in K2:
---
title: Kladde, dynamic, book an appointment
---
sequenceDiagram
autonumber
actor K as Customer
participant W as Web application
participant D as Azure SQL
participant G as Microsoft Graph
K->>W: fetch the page for a day [HTTPS, GET]
W->>D: read the day's appointments [TDS]
W-->>K: free times as HTML
K->>W: send time, name and email [HTTPS, POST]
W->>D: booking (unconfirmed) and mail in the outbox, one transaction
W-->>K: 303 to the page “Please confirm”
Note over W,G: in the background, seconds later
W->>G: confirmation mail from the outbox [HTTPS, sendMail]
K->>W: open the link in the mail [HTTPS, GET]
W->>D: store the booking as confirmed
autonumbernumbers the arrows so that you can say “step 5” in a conversation.->>is a call,-->>a response.Note over W,Gis a comment over two participants. Here it makes visible that step 8 doesn't run in the customer's request.
A deployment diagram shows where the containers run. Here Azure services appear with their names, environments and replicas:
---
title: Kladde, deployment, prod environment
---
flowchart TB
subgraph rg["Resource group rg-kladde-prod [Azure, region in the EU]"]
subgraph cae["Container Apps environment [Consumption plan]"]
web["Container App web<br/>2 replicas<br/>runs: web application"]
worker["Container App worker<br/>0 to 5 replicas, scales via Service Bus<br/>runs: import worker"]
job["Container Apps job tender-fetch<br/>at night on a schedule<br/>runs: TED fetch"]
end
sql[("Azure SQL Database<br/>sign-in with Entra ID only")]
storage[("Storage account<br/>container tenders-raw")]
bus[["Service Bus namespace<br/>queue tender-import"]]
vault["Key Vault"]
insights["Application Insights<br/>with Log Analytics workspace"]
end
A deployment diagram often needs no arrows at all: The connections are in the container diagram. It answers the question “what runs where, and how much of it?”, and the frames are enough for that.
In the same pull request as the code
A diagram that nobody maintains is worse than none after half a year, because it is believed. K4's house rules therefore say: The C4 model changes in the same pull request as the code it describes. Three habits make that easy:
- Keep it small. A context diagram with six boxes and a container diagram with ten can be adjusted in a minute. A picture with forty boxes can't.
- Generate what can be generated. The dependencies between the projects are written by
tools/deps.csfrom lesson 4.1.1. What a tool reads from the code, nobody has to maintain by hand. - Ask in the review. The pull request template gets an item: Is a container, a connection or an external system added
or removed? Then
docs/c4-model.mdis changed.
Exercise 1
Draw the Kladde's context diagram in Mermaid. It shows the Kladde as one box, all the groups of people who use it
and all the external systems it talks to. Every arrow says what flows and over which protocol. Put it into
docs/c4-model.md under the heading “Context”, with a line “Not shown”.
Get your inventory from lesson 4.1.1 out: The external systems are already in it.
How to check your result: Open the file on GitHub (or in your editor's preview). The picture appears without an error message. Show it to a person who doesn't know the Kladde and ask who uses the Kladde and what it depends on. Can she answer both questions without asking back?
Solution 1
## Context
```mermaid
---
title: Kladde, context
---
flowchart TB
team(["The office's team<br/>[Person]<br/>maintains contacts, appointments and tenders"])
customers(["Customers<br/>[Person]<br/>book an appointment"])
kladde["Kladde<br/>[Software system]<br/>contacts, appointments and tenders of a small IT office"]
entra["Microsoft Entra ID<br/>[Software system, external]<br/>signs in the team"]
ted["The EU's TED interface<br/>[Software system, external]<br/>public tenders"]
msgraph["Microsoft Graph with Exchange Online<br/>[Software system, external]<br/>sends mail from a mailbox"]
team -- "works with contacts, appointments, tenders<br/>[HTTPS, browser]" --> kladde
customers -- "book and confirm an appointment<br/>[HTTPS, browser]" --> kladde
kladde -- "has the team sign in, checks tokens<br/>[OpenID Connect]" --> entra
kladde -- "fetches new notices<br/>[HTTPS, JSON]" --> ted
kladde -- "sends booking confirmations<br/>[HTTPS, sendMail]" --> msgraph
msgraph -- "delivers the confirmation<br/>[email]" --> customers
```
Not shown: Azure as the platform (deployment diagram), the pipeline, Application Insights.
Common deviations and what to make of them:
- Azure SQL or Service Bus in the context diagram. They belong to the team and run for the Kladde, so they are containers of the Kladde and belong in the next picture. Only systems that belong to somebody else appear in the context.
- An arrow from Entra ID to the team. At sign-in, the Kladde redirects the browser to Entra ID, and the team signs in there.
An arrow
team --> entrawith “signs in [HTTPS, browser]” is therefore also correct and even more precise. What matters is that every arrow starts at the initiator. - A box “Users”. Too imprecise: Team and customers see different parts, sign in differently and have different permissions. That matters for every later decision, especially for the threat model.
Exercise 2
Draw the Kladde's container diagram. It shows all containers inside the system boundary, the people and external systems from the
context diagram, and every connection with a label. Put a legend underneath that explains shapes and lines, and a line
“Not shown”. Append both as a section “Containers” to docs/c4-model.md.
How to check your result: Go through the table of data flows from your inventory line by line. Every flow shows up again as an arrow, and every arrow has a label with square brackets. Count the unlabeled arrows: There must be zero of them.
Solution 2
## Containers
```mermaid
---
title: Kladde, containers
---
flowchart TB
team(["The office's team<br/>[Person]"])
customers(["Customers<br/>[Person]"])
subgraph kladde["Kladde [Software system]"]
web["Web application<br/>[Container: ASP.NET Core, Blazor SSR]<br/>pages, booking, sign-in, outbox"]
island["Island in the browser<br/>[Container: Blazor WebAssembly]<br/>choosing free times in the team area"]
fetch["TED fetch<br/>[Container: .NET, Container Apps job]<br/>fetches new notices at night"]
worker["Import worker<br/>[Container: .NET worker]<br/>processes notices"]
db[("Database<br/>[Container: Azure SQL Database]<br/>contacts, appointments, bookings, tenders, outbox")]
raw[("Raw data<br/>[Container: Blob Storage, tenders-raw]<br/>TED's notices, as delivered")]
queue[["Queue tender-import<br/>[Container: Service Bus]<br/>one reference per notice"]]
end
entra["Microsoft Entra ID<br/>[Software system, external]"]
ted["The EU's TED interface<br/>[Software system, external]"]
msgraph["Microsoft Graph<br/>[Software system, external]"]
team -- "pages and forms<br/>[HTTPS]" --> web
customers -- "book and confirm<br/>[HTTPS]" --> web
team -- "uses in the team area<br/>[browser]" --> island
island -- "loads free times<br/>[HTTPS, JSON]" --> web
web -- "reads and writes<br/>[TDS, EF Core]" --> db
web -- "has users sign in, checks tokens<br/>[OpenID Connect]" --> entra
web -- "sends mail from the outbox<br/>[HTTPS, sendMail]" --> msgraph
fetch -- "fetches new notices<br/>[HTTPS, JSON]" --> ted
fetch -- "stores raw data<br/>[HTTPS, Blob API]" --> raw
fetch -. "one reference per notice<br/>[AMQP]" .-> queue
worker -. "picks up references<br/>[AMQP]" .-> queue
worker -- "reads raw data<br/>[HTTPS, Blob API]" --> raw
worker -- "writes tenders<br/>[TDS, EF Core]" --> db
```
Legend: rounded = person, rectangle = container or system, cylinder = data store, double border = queue;
solid = call with response, dashed = via a queue, later.
Not shown: Application Insights (all containers send telemetry), Key Vault and the managed identities (deployment diagram),
the pipeline, the dependencies between the projects (tools/deps.cs).
Three places where solutions often diverge:
- Is the WebAssembly island a container of its own? Yes: It runs in the browser, not on the server, and talks to the web application over HTTPS. That is exactly what the picture has to show, because everything that runs in the browser can be read and changed by the user.
- Is the arrow from the worker to the web application missing? Your inventory found a project reference from
Kladde.WorkertoKladde.Web. That is a dependency in the build, not a call at runtime, and therefore doesn't belong in the container diagram. If the worker really calls the web application over HTTP, the arrow belongs in it. - How many boxes for Service Bus? One box per queue or topic that counts for the architecture, not one for the whole namespace. The namespace is a resource and appears in the deployment diagram.
Exercise 3
Draw the dynamic diagram “Import a tender” as a sequence diagram: from the schedule that starts the TED fetch to the
row in the database. Also show what happens to a notice that the worker can't process. For that, use an
alt block with two cases and a loop block for the notices.
How to check your result: Every participant in the sequence diagram is a container or an external system from your container diagram, with the same name. A participant that doesn't exist in the container diagram is a mistake in one of the two pictures.
Solution 3
## Dynamic: importing a tender
```mermaid
---
title: Kladde, dynamic, importing a tender
---
sequenceDiagram
autonumber
participant A as TED fetch
participant T as The EU's TED interface
participant R as Raw data
participant Q as Queue tender-import
participant W as Import worker
participant D as Database
Note over A: Container Apps job, at night on a schedule
A->>T: new notices since the last run [HTTPS, JSON]
T-->>A: notices
loop per notice
A->>R: store raw data [Blob API]
A->>Q: send reference, message ID = notice number [AMQP]
end
W->>Q: pick up and lock message [AMQP]
W->>R: read raw data [Blob API]
alt readable
W->>D: insert or update tender, by notice number [TDS]
W->>Q: complete message
else not readable
W->>Q: release message
Note over Q: after the maximum delivery count into the dead-letter queue, an alert reports it
end
```
Not shown: the alert itself and Application Insights.
Note the message ID: It is the notice number, not a random ID. Message and row thereby carry the same business identifier. If a notice arrives twice, the worker updates the same row instead of creating a second one, and with Service Bus duplicate detection (Standard tier and up), a second message within the configured time window is dropped even earlier. The diagram makes this decision visible, and that is why it is worth writing such details into the label.
The Kladde after this lesson
docs/c4-model.md contains the sections “Context” and “Containers” and the flow “Import a tender”, each with a legend and
“Not shown”. Put two sentences at the top of the file that say what it is and when it is changed:
# C4 model of the Kladde
Context, containers and important flows following the C4 model (c4model.com), as Mermaid. If a container, a connection or an
external system changes, this file changes in the same pull request.
Extend the pull request template by the item:
- [ ] Container, connection or external system changed? Then docs/c4-model.md in the same pull request
In docs/architecture.md from lesson 4.1.1, point to the C4 model. The inventory stays a snapshot from the end of K3; the
C4 model is the living description from now on.
What you can do now
- You tell context, containers, components and code apart and know that a container in the sense of C4 is not a Docker container.
- You use dynamic diagrams for flows and deployment diagrams for resources and replicas.
- You write diagrams in Mermaid with frames for the system boundary, shapes for people, stores and queues, and a legend.
- You label every element with name, kind, technology and purpose and every arrow with what flows and the protocol, directed from the initiator to the target.
- You note under every picture what it leaves out, and keep the model up to date in the same pull request as the code.
Further reading
- Architecture design diagrams: Microsoft's rules for architecture diagrams and the kinds of diagrams, from context to the data flow diagram for the threat model.
- c4model.com: Simon Brown's C4 model with terms, levels and notes on notation.
- Mermaid: Flowcharts and Mermaid: Sequence diagrams: the notation of both kinds of diagrams.