C4 Dynamic Diagram: Guide With Examples
A container diagram tells you that the API talks to the order service, the order service talks to Kafka, and the notification service reads from Kafka. It doesn't tell you what happens, in what order, when a customer clicks Place order. Is the payment taken before or after the order row is written? Does the confirmation email wait for the warehouse? Those are the questions people ask in an incident review, and the static diagrams can't answer them.
That's the job of the C4 dynamic diagram. It takes elements you have already drawn and numbers the interactions between them for one specific scenario. This guide covers what a dynamic diagram is, how it differs from a UML sequence diagram, when one is worth drawing (less often than you'd think), a complete worked example, the usual mistakes, and how to stop it going stale when the static model changes.
If you're new to C4, start with what the C4 model is. The worked example below builds on the kind of diagram covered in the container diagram guide.
What a dynamic diagram is
The dynamic diagram is one of the supplementary diagrams in the C4 model, alongside the system landscape and deployment diagrams. It isn't one of the four core levels. It sits next to them and borrows their elements.
The c4model.com definition is short:
- Scope: "A particular feature, story, use case, etc."
- Elements: "Your choice - you can show software systems, containers, or components interacting at runtime."
- Audience: "Technical and non-technical people, inside and outside the software development team."
- Recommended? "No, dynamic diagrams should be used sparingly to show interesting/recurring patterns or features that require a complicated set of interactions."
Two things follow from that definition.
First, a dynamic diagram shows instances of relationships you already have. If the container diagram has an arrow from the order service to Kafka, the dynamic diagram says "and in step 4 of checkout, that arrow is used to publish OrderPlaced". Structurizr's DSL makes this explicit: its documentation says that with a dynamic view, "you're showing instances of relationships that are defined in the static model", and the relationship has to exist there first (Structurizr DSL reference). That constraint is useful. It stops the dynamic diagram from inventing a call the static model doesn't know about.
Second, it's one scenario per diagram. Not "how the order service works", but "customer places an order, card payment, item in stock". The failure path gets its own diagram if it's worth drawing at all.
The ordering is shown with numbers on the arrows. That's the whole notation: the same boxes, the same arrows, plus a sequence number and a description of what happens at that step.
Dynamic vs sequence diagram
"C4 sequence diagram" is a common search, and the confusion is fair: the two diagrams answer the same question. The C4 site says the dynamic diagram can be drawn in two styles that carry the same information:
- Collaboration style. Boxes laid out freely (usually where they sit on the container diagram) with numbered arrows between them. C4 notes this style is based on the UML communication diagram, previously called the collaboration diagram.
- Sequence style. Elements as columns across the top, time running down the page, arrows between lifelines. It looks like a UML sequence diagram, but the participants are C4 elements.
So a dynamic diagram in sequence style is a kind of sequence diagram. The real differences are with a classic UML sequence diagram drawn from code:
| C4 dynamic diagram | UML sequence diagram (typical use) | |
|---|---|---|
| Participants | Systems, containers or components from your C4 model | Objects, classes, often method-level |
| What an arrow means | A use of a relationship in the static model, with its protocol | A message or method call |
| Level of detail | Architectural: "publishes OrderPlaced (Kafka)" |
Often implementation: validate(), save(), return values |
| Notation | Boxes and numbered arrows, a key explains anything unusual | Lifelines, activation bars, combined fragments (alt, loop, par) |
| Connection to other diagrams | Reuses elements from the container or component diagram | Usually standalone |
Use the collaboration style when the spatial layout carries meaning, for example when readers already know the container diagram and you want the flow to appear over it. Use the sequence style when ordering is the whole point, there are more than eight or so steps, or there's a lot of back-and-forth between two elements (request, response, callback). Neither is more correct; C4 leaves the choice to you.
If you need alt and loop fragments to explain a scenario, that's often a sign you're describing an algorithm rather than an architecture. Draw the architectural version as a dynamic diagram, and leave the detailed version to a UML sequence diagram next to the code, if anyone needs it. Our C4 vs UML comparison covers where each notation fits.
When one is worth drawing (and when it isn't)
C4's own answer to "recommended?" is no, which is worth taking seriously. Every dynamic diagram is another artifact that has to change when the architecture changes. Draw one when the scenario meets at least one of these:
- The order isn't obvious from the static diagram. Checkout, payment capture, a saga that compensates on failure. If a senior engineer on the team would get the order wrong, draw it.
- The scenario crosses several containers or systems. Anything that touches four or more containers, or leaves your system and comes back (webhooks, callbacks, third-party redirects like 3-D Secure).
- It's asynchronous. Once a queue is involved, the static diagram shows that A and B both touch Kafka but not that B runs after A, or that A doesn't wait for it.
- It recurs. A pattern used in many places (how every service authenticates a request, how every write emits an event) is worth one diagram that the rest of the docs can point to.
- Someone asks for it in a review or an incident. The best trigger there is. If an incident review spent twenty minutes reconstructing a sequence on a whiteboard, that sequence deserves a diagram.
Skip it when:
- The flow is a straight line. Browser, API, database, back. The container diagram already says that.
- It's CRUD. Five dynamic diagrams for create, read, update, delete and list add nothing.
- Nobody will read it. A dynamic diagram for every user story is a documentation backlog, not documentation.
A reasonable target for a typical product is a handful: the two or three journeys that make money or wake people up, plus one or two recurring patterns.
Worked example: "customer places an order"
Take the e-commerce system from our complete guide. Its container diagram has a React single-page app, a Kong API gateway, Go services for orders, products and users (each with its own PostgreSQL database), Kafka, and a notification service. At level 1 the system also talks to Stripe as a payment gateway and SendGrid for email.
Here are the relationships from the static model that this scenario uses. Every step below must map to one of them.
[Customer] --> [Single-Page Application (React)] : Uses (HTTPS)
[Single-Page Application] --> [API Gateway (Kong)] : Makes API calls (HTTPS/JSON)
[API Gateway] --> [Order Service (Go)] : Routes requests
[Order Service] --> [Product Service (Go)] : Checks stock (gRPC)
[Order Service] --> [Payment Gateway (Stripe)] : Authorizes payments (HTTPS/REST)
[Order Service] --> [Order Database (PostgreSQL)] : Reads/writes orders (SQL)
[Order Service] --> [Message Queue (Kafka)] : Publishes order events
[Notification Service (Go)] --> [Message Queue] : Consumes order events
[Notification Service] --> [Email Service (SendGrid)] : Sends email (HTTPS)
The dynamic diagram, collaboration style
The numbered interactions, drawn over those same boxes:
1. [Customer] -> [Single-Page Application] : Clicks "Place order"
2. [Single-Page Application] -> [API Gateway] : POST /orders (HTTPS/JSON)
3. [API Gateway] -> [Order Service] : Routes the authenticated request
4. [Order Service] -> [Product Service] : Reserves stock for each line item (gRPC)
5. [Order Service] -> [Payment Gateway (Stripe)] : Authorizes the card for the order total (HTTPS)
6. [Order Service] -> [Order Database] : Writes the order with status "placed" (SQL)
7. [Order Service] -> [Message Queue] : Publishes OrderPlaced (Kafka)
8. [Order Service] -> [Single-Page Application] : Returns 201 with the order number (via the gateway)
9. [Notification Service] -> [Message Queue] : Consumes OrderPlaced (Kafka)
10. [Notification Service] -> [Email Service (SendGrid)] : Sends the confirmation email (HTTPS)
Laid out on the container diagram, the numbers tell the story: steps 1 to 8 are synchronous and happen while the customer waits, steps 9 and 10 happen afterwards and the customer never waits for them.
The same scenario, sequence style
| # | From | To | What happens | Sync? |
|---|---|---|---|---|
| 1 | Customer | Single-Page Application | Clicks "Place order" | yes |
| 2 | Single-Page Application | API Gateway | POST /orders |
yes |
| 3 | API Gateway | Order Service | Routes the request | yes |
| 4 | Order Service | Product Service | Reserves stock | yes |
| 5 | Order Service | Payment Gateway (Stripe) | Authorizes the card | yes |
| 6 | Order Service | Order Database | Writes the order | yes |
| 7 | Order Service | Message Queue | Publishes OrderPlaced |
no (fire and forget) |
| 8 | Order Service | Single-Page Application | Returns 201 with the order number | yes |
| 9 | Notification Service | Message Queue | Consumes OrderPlaced |
async |
| 10 | Notification Service | Email Service (SendGrid) | Sends confirmation | async |
A table like this is a perfectly good way to write a dynamic diagram down. Drawn as lifelines, it's the sequence style.
What the diagram tells you
Reading the ten steps, you can answer questions the container diagram couldn't:
- What happens if Stripe is down? Stock is already reserved at step 4 when authorization fails at step 5. Someone has to release it. The diagram makes it obvious that the order service needs a compensation path, or that steps 4 and 5 should swap.
- Can the customer get a confirmation for an order that doesn't exist? No. The event is published at step 7, after the write at step 6. If those two were the other way round, a failed write could still send an email. (If you need the write and the publish to be atomic, that's where an outbox table comes in, and it's worth an ADR.)
- What's on the customer's critical path? Steps 2 to 8. Email isn't, which is why it goes through Kafka.
Here's the same scenario in Structurizr DSL, for teams that keep their model as code. It only compiles if each relationship exists in the static model, which is the constraint described above:
dynamic webshop "PlaceOrder" "Customer places an order" {
customer -> spa "Clicks Place order"
spa -> gateway "POST /orders"
gateway -> orderService "Routes the request"
orderService -> productService "Reserves stock"
orderService -> stripe "Authorizes the card"
orderService -> orderDb "Writes the order"
orderService -> kafka "Publishes OrderPlaced"
notificationService -> kafka "Consumes OrderPlaced"
notificationService -> sendgrid "Sends confirmation"
autoLayout lr
}
Step 8, the response, isn't a separate relationship in the static model, so it's left out of the DSL version. Responses are usually implied by the request; draw them only when the response itself matters.
Common mistakes
Too many steps
A dynamic diagram with thirty numbered arrows is a sequence nobody can hold in their head. If a scenario runs past about fifteen steps, split it: "checkout, up to payment" and "checkout, after payment", or one diagram per system the flow crosses. Our own flows documentation suggests 5 to 15 steps per flow for the same reason.
Mixing levels
C4 lets you choose the level (systems, containers or components), but pick one per diagram. A diagram where step 3 goes to the "Order Service" container and step 4 goes to the PaymentClient component inside it forces the reader to switch zoom mid-story. If a step needs component detail, draw a second dynamic diagram scoped to that container.
Arrows that don't exist in the static model
If the dynamic diagram shows the notification service calling the order service directly, and the container diagram doesn't have that relationship, one of the two is wrong. Usually it's the dynamic diagram, drawn from memory. Treat the static model as the source of truth and make every step reference one of its relationships.
Drawing every call
Health checks, token refreshes, log shipping and metrics scrapes are real, but they're not the scenario. Leave out anything that would appear in every dynamic diagram you draw. If it matters, it gets its own recurring-pattern diagram once.
Hiding async behind sync-looking arrows
Steps 9 and 10 above happen after the customer already has a response. If they're drawn with the same arrows as steps 1 to 8, readers assume the email is sent before the page loads. Mark asynchronous steps (a dashed line, an "async" label, or a separate numbering like 9a) and say what the convention is in the key.
Leaving out the failure that matters
A happy-path diagram is the right default. But if the reason you're drawing the flow is "what happens when payment fails", draw that path, not the happy one.
Keeping it true when the static model changes
A dynamic diagram depends on the static model twice: on its elements and on its relationships. That makes it one of the first things to go stale. Someone renames the order service to "checkout service", replaces Kafka with SQS, or moves stock reservation into a new inventory service, and every dynamic diagram that touched those boxes is now wrong. Nothing tells you.
Three habits help:
- Draw from the model, not next to it. A dynamic diagram in a drawing tool is a copy of the container diagram, and copies drift. A dynamic view that references model elements by identifier (Structurizr DSL does this) at least picks up renames, and fails loudly when a relationship disappears.
- Keep the list short. Five dynamic diagrams you check each quarter beat thirty you never open.
- Review them when the containers they touch change. When a pull request changes a container or a relationship, the dynamic diagrams that use it are part of the review.
How flows work in archyl
In archyl, a dynamic diagram is a Flow: an ordered list of steps, each with a source element, a target element, a relationship and a description, played back step by step over the diagram (flows documentation). You can build one by hand by picking relationships from your model, or describe the scenario and let the AI flow generator draft the steps from your C4 model. The generator validates every step against the model before saving it: each step's source and target must exist, and the relationship it cites must connect those two elements. A step that doesn't match is dropped rather than drawn.
Two limits, stated plainly because they're exactly the problem this section is about:
- A flow keeps a snapshot of the elements and relationships it uses, taken when a step is added. That keeps a flow readable even if an element is later deleted, but it also means renaming a container in the model doesn't rename it in existing flows. When the model changes, open the flows that touch it and check them.
- The drift score doesn't check behavior. archyl's drift score tells you whether the documented elements still exist in the code. If a synchronous call between two services becomes a queue message and nothing is renamed or moved, the score doesn't change, and neither does the flow.
For more on the practice side, including how we write flows as documents with preconditions and error handling, see documenting user flows.
FAQ
Is the dynamic diagram part of the C4 model?
Yes, as a supplementary diagram. The four core levels are System Context, Container, Component and Code. The C4 model adds three supplementary diagrams: system landscape, dynamic and deployment. The dynamic diagram reuses elements from the core levels and shows how they interact for one scenario.
What's the difference between a C4 dynamic diagram and a sequence diagram?
A C4 dynamic diagram can be drawn in a collaboration style (free layout, numbered arrows) or a sequence style (lifelines, time running down). The sequence style looks like a UML sequence diagram, but its participants are C4 systems, containers or components, and each arrow is a use of a relationship from the static model, not a method call.
Which level should a dynamic diagram use?
Whichever level answers the question, and only one per diagram. Container level is the most common, because most scenarios worth drawing cross several deployable units. Use system level for flows between systems and component level to explain the inside of one container.
How many steps should a dynamic diagram have?
There's no official limit. Past about fifteen steps, most readers lose track, so split the scenario into parts or draw one diagram per system it crosses.
Can a C4 dynamic diagram show asynchronous messaging?
Yes. Show the publish and the consume as separate numbered steps, and make it visible which steps the caller waits for and which it doesn't: a dashed line, an "async" label, or a separate numbering scheme, explained in the diagram key.
Does archyl support C4 dynamic diagrams?
Yes, as Flows. Each step references a source element, a target element and a relationship from your model, and the flow plays back step by step over the diagram. You can author flows by hand or generate a draft from a text description. Flows keep a snapshot of the elements they use, so review them when the containers they touch change.
Want to draw your first flow over a model that already exists? Try archyl free and generate the C4 model from your code first. Keep reading: What is the C4 Model? A Complete Guide | C4 Container Diagram Guide | Documenting User Flows | Flows documentation.