Drawing UML sequence diagrams
Open in the interactive app: practice, quiz, mock interview →In one minute
- A sequence diagram shows one scenario as messages between participants over time: participants across the top, time running down, one arrow per message.
- Each participant has a lifeline (dashed vertical line) and activation bars showing when it is busy handling a call. Solid arrows with filled heads are synchronous calls, open heads are asynchronous sends, dashed arrows are replies.
- Combined fragments (
alt,opt,loop,par,break,critical) add branching, repetition and concurrency without turning the diagram into a flowchart. - At runtime a synchronous arrow is a stack frame inside one process and a pair of client and server spans across processes. A distributed trace is a sequence diagram that the system drew for you.
- Draw one when the question is "who calls whom, in what order, and what happens when step 4 fails". Draw a class diagram for structure and an activity diagram for a process with many branches.
How it works
Reading the axes
Horizontal position means nothing except identity: each column is one participant, which can be an object, a service, a queue or a human actor. Vertical position is time, but only as order. Equal spacing does not mean equal duration, and two arrows at the same height on different lifelines are not necessarily simultaneous.
Each lifeline is a total order of events: on the pricing lifeline, receiving Quote happens before sending GET rules. Across lifelines the order is partial. A message's send happens before its receive, and that chains through, which is exactly Lamport's happens-before relation drawn on paper. Two events on different lifelines with no chain of messages between them are concurrent, whatever their height on the page.
Messages
A solid line with a filled arrowhead is a synchronous call: the sender blocks until the reply. A solid line with an open arrowhead is an asynchronous send: the sender continues. A dashed line back is a reply, labelled with the return value or status. In Mermaid these are ->>, -) and -->>, and -x or --x ends a message with a cross to show it was lost or failed.
The activation bar on the receiver starts when a synchronous call arrives and ends when the reply leaves. Nested bars show a participant calling itself or being re-entered by a callback, which is often the first visual hint of a reentrancy bug.
An arrow into a participant box drawn halfway down is a constructor call, and an X ending a lifeline is destruction (Mermaid: create participant, destroy).
Combined fragments
A fragment is a labelled box around some messages:
altwith guards such as[card accepted]and[declined]is if/else.optis analtwith one branch.loop [for each item]repeats its contents.parsplits into regions separated by dashed lines that run concurrently; the fragment ends when all regions finish.breakruns its contents and then abandons the rest of the enclosing interaction, the diagram's version of an early return or thrown exception.criticalmarks a region that must not interleave with others, such as a lock or a transaction.
Nesting more than two fragments deep is a sign the diagram is trying to be a program. Split it into one diagram per scenario: the happy path, the declined card, the inventory timeout.
Reviewing agent-written diagrams
Agents generating diagrams from code tend to draw every call as synchronous, omit failure replies, and put par around calls the code awaits one after another. Compare with a real trace: spans that never overlap were not parallel. The practice task generates the diagram from spans.
The same shape elsewhere
Sequence diagrams are the ordering shape. A database transaction history and a TCP capture (Wireshark's flow graph is a sequence diagram of segments) raise the same questions: what is ordered, what is concurrent, and what happens when one message is lost.
Down the stack
Layer 1The contract
A sequence diagram claims four things that can be checked against a running system. Messages on a lifeline occur in the drawn order. Every synchronous call gets exactly one reply or one failure, and the caller's activation encloses the callee's. Messages in different par regions may interleave in any order. Messages not shown are not part of the scenario, which is a weaker claim than "do not happen". The diagram says nothing about absolute time, so latency budgets belong in notes or in a separate SLO.
Layer 2The mechanism
Inside one process a synchronous arrow is a function call and the activation bar is the callee's stack frame: pushed on call, popped on return. A debugger's stack view at any instant is a vertical slice through the diagram's nested activations.
Across processes the mapping is distributed tracing (see the Distributed Tracing lesson for propagation and sampling). OpenTelemetry records each operation as a span with trace_id (16 bytes), span_id (8 bytes), parent_span_id, name, kind, start_time_unix_nano, end_time_unix_nano and status. One arrow between services is a pair of spans: a CLIENT span in the caller and a SERVER span in the callee whose parent is that client span, linked by the W3C traceparent header, a 55-character string such as 00-<32 hex trace id>-<16 hex parent id>-01. An asynchronous send is a PRODUCER span and a CONSUMER span, often connected with a span link rather than a parent, because the consumer may run long after the producer has finished. INTERNAL spans are work inside one participant and draw as nothing, or as a self-call.
A par fragment corresponds to sibling spans whose intervals overlap. A loop is a run of sibling spans with the same name. An alt is not visible in one trace at all; it appears only across many traces, as different children under the same parent.
Mermaid parses sequenceDiagram text with a Jison-generated grammar into participants and an ordered list of message and fragment events, then renders SVG itself. No graph layout engine is needed: participants are columns, events are rows, and the real work is measuring label text. PlantUML likewise lays out sequence diagrams without Graphviz. Jaeger draws the same data as a Gantt chart, keeping durations but hiding who called whom.
Layer 3The machine
On x86-64 a call pushes an 8-byte return address and the callee typically sets up a frame of a few dozen to a few hundred bytes; a call and return costs a few nanoseconds when the branch predictor guesses right. A cross-service arrow costs from about 100 microseconds within a rack to tens of milliseconds across regions, so a diagram with 12 synchronous hops in sequence has a latency floor you can read off the page before writing any code.
async and await break the frame mapping. Rust futures, C# async methods and JavaScript promises turn the suspended activation into a heap object holding the local variables, and the thread's stack unwinds while the call is pending. The diagram still shows one nested activation, but a stack trace taken in the callee no longer contains the caller. That is why runtimes added async stack traces and why tracing context travels in task-local or context objects instead of the stack.
Span timestamps come from different machines. NTP usually keeps hosts within a millisecond or so in one datacenter, but a fast child call can still appear to start before its parent or end after it. Trace viewers adjust for this skew, and any tool that infers ordering or parallelism from timestamps must tolerate it.
Why it's built this way
- Most design bugs are ordering bugs (a call made before a lock is taken, a reply lost after a timeout), so the notation makes order the main axis.
- Each participant runs sequentially but participants run concurrently, so each lifeline is a total order and the picture as a whole is only a partial order.
- Real interactions branch and repeat, so fragments add control flow without abandoning the time axis.
- Hand-drawn diagrams drift from code, so tracing systems record the same structure automatically, and the diagram can be regenerated from evidence.
Diagram
sequenceDiagram
participant C as Client
participant G as gateway
participant P as pricing
participant I as inventory
participant Pay as payments
C->>G: POST /checkout
par quote and reserve
G->>P: Quote
P-->>G: total
and
G->>I: Reserve
I-->>G: hold id
end
G->>Pay: Charge
alt card accepted
Pay-->>G: charge id
G-->>C: 201 Created
else declined
Pay--xG: declined
G-)I: Release hold
G-->>C: 402 Payment Required
endWorked example
A checkout endpoint has a p50 of 180 ms and a target of 120 ms. The design doc's sequence diagram shows the gateway calling pricing and inventory inside a par fragment, then payments. Everyone believes pricing and inventory run in parallel.
An engineer pulls 500 traces and generates a diagram from each. In 480 of them the Quote and Reserve spans do not overlap: Quote runs from 30 to 70 ms and Reserve from 71 to 129 ms. The code awaited each call in turn:
quote = await pricing.quote(cart)
hold = await inventory.reserve(cart)Running them with asyncio.gather makes them overlap, and the critical path drops from 40 + 58 = 98 ms to max(40, 58) = 58 ms, taking p50 from 180 to about 140 ms. The rest of the gap turns out to be payments waiting on an undersized connection pool.
The other 20 traces matter more: Reserve failed and the gateway still called Charge. The design diagram had no inventory failure branch, so nobody had designed one. The fix adds a break fragment to the diagram and an early return to the code.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Hand-drawn sequence diagram | Shows intent, includes failure paths you plan for | Drifts from code, can be wrong about parallelism | Design review, before code exists |
| Diagram as code (Mermaid, PlantUML) | Diffable, reviewed with code | Still written by hand | Design docs in the repository |
| Generated from traces | Shows what really happened | One run only, no intent, sampling gaps | Debugging, verifying a design |
| Trace Gantt view | Real durations and overlap | Hides caller and callee relationships | Latency analysis |
Pitfalls in production
pardrawn around calls the code runs sequentially. Fix: check span overlap in traces, and treat the trace as the truth.- Only the happy path is drawn, so retries, timeouts and partial failure are undesigned. Fix: one diagram per important failure scenario, or
altandbreakfragments for the main ones. - Async sends drawn as synchronous calls, hiding that the caller does not wait and the consumer may run much later. Fix: use open arrowheads and show the queue as its own participant.
- Diagrams with 15 participants and nested fragments nobody can read. Fix: one diagram per scenario, and collapse internal services behind one participant.
- Tools that infer order from timestamps report a child starting before its parent because of clock skew. Fix: order by parent and child structure first, then by time within siblings.
Leadership lens
- Require a sequence diagram for every new cross-service flow, including at least one failure scenario. Count the synchronous hops in review; each one is latency and an availability multiplier.
- Ask for traces that confirm the diagram once the feature ships. Disagreements between diagram and trace are cheap bugs found early.
- Prefer diagrams as code in the repository so they are reviewed with the change that makes them wrong.
- Use generated diagrams in incident reviews; they settle "who called whom" arguments faster than logs.
Check yourself
Two arrows appear at the same height on different lifelines. Did they happen at the same time?
Show answer
Not necessarily. Height orders events only within a lifeline and along message chains; events with no chain between them are concurrent and may happen in either order.
What pair of OpenTelemetry spans corresponds to one synchronous arrow between two services?
Show answer
A
CLIENTspan in the caller and aSERVERspan in the callee, whose parent is the client span, connected by the propagatedtraceparent.Why does a stack trace from an
asynccallee often not show the caller, while the sequence diagram does?Show answer
The suspended caller's activation lives in a heap-allocated state object, not on the thread's stack, so unwinding the stack does not reach it.
How would you check that a
parfragment in the design is real?Show answer
Look at traces: the sibling spans inside it must overlap in time. If they never overlap, the code is sequential.
Watch
- How to Make a UML Sequence Diagram (Lucid Software): lifelines, activations, message types and fragments step by step.
Free resources
- Sequence diagram (Wikipedia): notation overview and history.
- Sequence diagrams (Mermaid): arrow types, fragments, create and destroy, activations.
- Sequence diagram (PlantUML): the same notation in PlantUML, with more layout control.
- Traces (OpenTelemetry): span fields, span kinds and links, the runtime counterpart of each arrow.
- Trace Context (W3C): the
traceparentheader that connects caller and callee spans. - Dapper (Google Research): the paper that made trace trees standard practice.