Using UML Sequence Diagrams to Document Asynchronous, Event-Driven Systems
Event-driven systems hide their behavior across services and topics. UML sequence diagrams — with async arrows, found messages, and alt/par fragments — make that chronology visible without the bloat.
07 Nov 2025, 17:30 UTC

Your order service publishes an event, three consumers react to it, one of them occasionally times out and retries, and somewhere in that chain a duplicate message gets processed twice. When someone asks "what actually happens when a customer checks out?", the honest answer is usually spread across five repositories and a message broker's topic list. This is the gap a UML sequence diagram fills well — not as documentation theater, but as a shared chronological map of who sends what, to whom, and whether anyone waits for a reply.
The thesis here is simple: sequence diagrams are one of the few UML diagrams that survive contact with event-driven architecture, provided you use the async notation honestly and resist the urge to model every edge case.
Why sequence diagrams fit event-driven flows
A sequence diagram shows lifelines (vertical lines representing participants) and messages (horizontal arrows) ordered top-to-bottom in time. That chronological spine is exactly what distributed traces and log spelunking give you painfully — the diagram gives it to you at a glance.
Three pieces of UML 2.x notation do most of the work for async systems:
- Asynchronous signals — open-headed arrows (a stick arrowhead rather than a filled triangle). The sender does not block. This is the honest way to draw "publish to topic" or "enqueue task." A filled, solid arrowhead means a synchronous call where the sender waits; mixing these up makes the diagram lie about your system's coupling.
- Found messages — an arrow that starts from an open circle or the diagram edge rather than from a lifeline. Use it for external triggers (a webhook, a cron tick, a user action) without having to invent an actor whose internals you don't care about.
- Combined fragments — labeled boxes that wrap part of the interaction.
altcovers mutually exclusive branches (success vs. failure),optcovers an optional step,loopcovers retries, andparmarks regions that run concurrently — useful when one event fans out to several independent consumers.
A worked example: order checkout fan-out
Here is a checkout flow rendered in PlantUML, which you can run anywhere PlantUML is installed (or via its web server) with no special permissions — it's plain text in, image out:
@startuml
participant "Order API" as API
participant "Broker" as MQ
participant "Inventory" as INV
participant "Payments" as PAY
participant "Notifications" as NOTIFY
o-- API : checkout request (found message)
API -> MQ : OrderCreated (async)
par concurrent consumers
MQ -> INV : OrderCreated
INV -> MQ : StockReserved
else
MQ -> PAY : OrderCreated
alt payment succeeds
PAY -> MQ : PaymentCaptured
else payment fails
PAY -> MQ : PaymentFailed
end
else
MQ -> NOTIFY : OrderCreated
end
alt PaymentFailed observed
API -> MQ : CancelOrder
MQ -> INV : ReleaseStock
end
@enduml
A few deliberate choices in that sketch: every broker interaction is an async arrow because none of these services block on each other. The par fragment communicates that inventory, payments, and notifications consume the same event independently — there is no implied ordering between them, which is usually the fact people get wrong. The failure path lives in a separate alt instead of being woven through the happy path, which keeps the main flow readable.
To verify the diagram against reality, run one checkout in a staging environment and pull the broker's message log or a distributed trace (any OpenTelemetry collector output works). Each message in the trace should correspond to an arrow in the diagram, in the same relative order within each consumer. If the trace shows messages your diagram doesn't, the diagram is stale — which is itself useful information.
The trade-off: diagram bloat and drift
The failure mode is over-modeling. If you try to capture every retry policy, dead-letter queue, and idempotency check in one diagram, you end up with something harder to read than the code. A practical rule: one diagram per business-meaningful interaction ("checkout", "refund", "password reset"), showing the happy path plus the one or two failure branches a newcomer actually needs. Retries and DLQ handling usually deserve their own small diagram or a sentence of prose.
The second limitation is drift. UML describes the system; it doesn't generate or enforce it. The diagram will silently become wrong as the code evolves unless something pulls it back. Text-based formats like PlantUML or Mermaid help because the diagram lives in the repo and changes show up in code review — a reviewer who sees a new consumer added to a topic can ask why the diagram didn't change. That review habit matters more than notation purity.
Finally, don't sweat strict UML 2.5 compliance. If your team reads an open arrowhead as "async" and a box labeled alt as "one of these happens," you're getting the value. Formal correctness is for specifications; sketches are for alignment.
Where to start
Pick the one interaction in your system that generates the most "wait, what happens after X?" questions. Draft it as a PlantUML sequence diagram with async arrows, one par for the fan-out, and one alt for the failure path that bites you most often. Commit it next to the code, then validate it once against a real trace. If a new hire can predict the message flow from the diagram before reading the code, it earned its place.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.