Drawing systems and processes

Sequence diagrams

Deep diveAbout 26 minutesMermaid 12 AI-generated

A sequence diagram shows the messages that pass between parts of a system during one interaction, in the order they happen. A message might be an HTTP request, a database query or the reply to either. Each part gets a vertical line, and time runs down each line. Each arrow is one message, and only the arrows order events on different lines.

A drawing of boxes and arrows can show that an API talks to a database and to an email provider. A sequence diagram shows which call comes first, which part sits waiting while another works, and what each side knows when a reply never comes back. You reach for one when that order is the question. You might be designing a request that crosses several services, working out why a page is slow, or deciding what should happen if a service never answers.

The cost is that the arrows draw one path through the interaction. Steps that happen only sometimes, repeat or fail need a frame around them or a diagram of their own, and a drawing that tries to hold every case soon gets too crowded to read. It also says nothing about how the system is built or how one record changes over many runs. Drawing systems and processes matches those questions to the diagrams that answer them.

Every example here follows one invented repair service. Mina books a dishwasher repair in her browser. A Repair API stores the booking in a PostgreSQL database, Repair DB. A notification worker, a separate process running in the background, later emails her through an external email provider. Up to the design readings near the end, each section changes one thing about that booking.

Lifelines and messages

Each part of the system in a sequence diagram is a participant, drawn as a named box at the top. A lifeline is the dashed line running down from that box. A message is an arrow from the sender’s lifeline to the receiver’s, labelled with what is asked or answered.

Read the four messages of Mina’s booking from top to bottom.

Figure 1

Mina books a repair

BrowserRepair APIRepair DBPOST /repairsINSERT repair requestCOMMIT ok201 Created: R17

The browser sends POST /repairs to the Repair API. The API inserts the repair request into Repair DB, the database replies that the commit succeeded, and only then does the API reply 201 Created with request R17.

Messages in order, as text
  1. Browser → Repair API: POST /repairs (call)
  2. Repair API → Repair DB: INSERT repair request (call)
  3. Repair DB → Repair API: COMMIT ok (reply)
  4. Repair API → Browser: 201 Created: R17 (reply)
Mermaid source
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    participant DB as Repair DB
    UI->>API: POST /repairs
    API->>DB: INSERT repair request
    DB-->>API: COMMIT ok
    API-->>UI: 201 Created: R17

The API can’t answer the browser until the database has answered the API. Reading down the page gives you that order without reading any code. Left to right carries no meaning. Put whoever starts the interaction on the left, so requests travel right and replies come back left.

Most figures keep their text form, in Mermaid, under Mermaid source, closed until you want to copy it. Figure 1’s source reads like this: each participant line declares a lifeline, as gives the name shown in its box, and the order of those lines sets the order from left to right. ->> draws a call and -->> its reply. Writing a diagram as text says more about the format.

Label each arrow with the operation and the data that matters, such as POST /repairs or 201 Created: R17. A label like “data” or “response” hides whether the database is being asked to read, write or confirm something.

A lifeline stands for a role, not a machine. “Repair API” means whichever copy of the API handles the request, and one lifeline is enough while the story doesn’t depend on which copy that is.

When it does, give each copy its own lifeline. Two workers that claim the same job, or two API instances that both accept a cancellation, need a lifeline each, or the race has nowhere to appear.

Replies and activation bars

Calls are the solid arrows in the booking: the sender asks, then waits for the answer. The dotted arrows are the replies. Between sending a call and getting its reply, the caller does nothing else for that request. An activation bar is a narrow bar on a lifeline that shows when that participant is handling a call.

The next animation adds activation bars to the booking, plus a hatched bar for waiting. The hatched bar is this article’s addition, not standard notation. It darkens the longer the wait goes on. The word under each name says what that participant is doing. Watch the browser’s bar while the database works. Then set the database to 3 s and watch again.

Figure 2

Who is waiting

BrowserRepair APIRepair DBdonehas R17idleidlePOST /repairsINSERT repair requestCOMMIT ok201 Created: R17

The same booking with activation bars. The browser waits from sending POST /repairs until 201 Created arrives. The API is busy handling the request, and inside that it waits 50 ms for Repair DB. The diagram is the same height whatever the database time.

Messages in order, as text
  1. Browser → Repair API: POST /repairs (call)
  2. Repair API → Repair DB: INSERT repair request (call)
  3. Repair DB → Repair API: COMMIT ok (reply)
  4. Repair API → Browser: 201 Created: R17 (reply)
Mermaid source
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    participant DB as Repair DB
    UI->>+API: POST /repairs
    API->>+DB: INSERT repair request
    DB-->>-API: COMMIT ok
    API-->>-UI: 201 Created: R17

The browser waits for the whole chain. With a 3-second database write it waits a little over 3 seconds, and the API waits almost as long inside it. The API’s wait is nested inside the browser’s, so each extra second in the database is an extra second for Mina.

The diagram didn’t get taller. Vertical distance shows order, not elapsed time, so a 5 ms write and a 3-second write draw the same picture. When the question is how long something took, write the times in a note, a box of text attached to a lifeline, or draw a timeline.

UML, the standard notation these diagrams come from, calls an activation bar an execution specification. Mermaid and PlantUML draw one kind of bar wherever you ask for it, so they can’t tell working from waiting.

Not every diagram needs the bars or every reply. Draw a reply when it carries data or a decision, such as 201 Created: R17 or a 422. A bare “OK” that only closes the call can go, as long as nothing in the design hangs on that reply failing to arrive. Draw activation bars when who is blocked is the question, as in this section, and leave them off when it isn’t. Most diagrams later in this article leave them off.

Synchronous and asynchronous messages

Every call so far has been a synchronous message: the sender waits for the reply. UML draws it with a filled arrowhead. An asynchronous message is one the sender doesn’t wait for. It has an open arrowhead, and the sender carries straight on.

The two words describe the sender, not the protocol. A browser’s await fetch(...) is a synchronous message in a diagram, because the code stops until the reply comes, even though JavaScript calls the style asynchronous. An HTTP request whose response nobody reads is an asynchronous message, even though HTTP is a request and response protocol. Label each arrow by what its sender does next.

Suppose the API also sends Mina’s confirmation email before it answers her. The email provider takes 2.5 seconds to reply, an invented figure. Watch the API wait for the provider first. Then switch the API to Doesn’t wait and see when the browser gets its 201 Created.

Figure 3

Sending the email inside the request

BrowserRepair APIRepair DBEmail providerdonehas R17idleidleidlePOST /repairsINSERT repair requestCOMMIT okPOST /emails202 Accepted201 Created: R17

The API stores the request, then calls the email provider and waits for its reply. The provider takes 2.5 seconds, so the browser waits more than 2.5 seconds for 201 Created.

Messages in order, as text
  1. Browser → Repair API: POST /repairs (call)
  2. Repair API → Repair DB: INSERT repair request (call)
  3. Repair DB → Repair API: COMMIT ok (reply)
  4. Repair API → Email provider: POST /emails (call)
  5. Email provider → Repair API: 202 Accepted (reply)
  6. Repair API → Browser: 201 Created: R17 (reply)
Mermaid source, when the API doesn’t wait
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    participant Provider as Email provider
    UI->>API: POST /repairs
    API-)Provider: POST /emails
    API-->>UI: 201 Created: R17
    Provider-->>API: 202 Accepted
    Note over API: Nobody is waiting for this reply

When the API waits, Mina waits too, for more than 2.5 seconds. If the provider were down, her booking would hang or fail, even though the database had already stored it.

When the API doesn’t wait, the browser gets its 201 Created in about 70 ms. The provider’s 202 Accepted comes back 2.5 seconds later to an API that has moved on. Nothing is waiting for it. Had that reply been an error, nothing would have handled it. The email would never have been sent, and nobody would know.

Neither version suits a booking. The first makes Mina wait for an email. The second ignores any error the provider sends back. The repair service moves the email out of the request instead.

Background workers and the transactional outbox

The API now records the email as work to do instead of sending it. It writes notification job J9 to Repair DB in the same transaction as the booking. A separate process, the notification worker, picks the job up later and calls the provider.

The animation adds the worker and the provider as two more lifelines. Watch when the browser gets R17 compared with when the email goes out.

Figure 4

A job in the database

BrowserRepair APIRepair DBNotificationworkerEmail providerdonehas R17idleidledoneJ9 sentsent1 emailNext poll, 0.7 s laterPOST /repairsINSERT request + job J9COMMIT ok201 Created: R17Claim a job for 5 minJob J9POST /emails202 AcceptedMark J9 sentOK

The booking is stored with notification job J9 in one transaction and the browser gets R17 within about 70 ms. On its next poll, 0.7 seconds later, the worker claims J9 for 5 minutes. The provider accepts the email and the worker marks J9 sent.

Messages in order, as text
  1. Browser → Repair API: POST /repairs (call)
  2. Repair API → Repair DB: INSERT request + job J9 (call)
  3. Repair DB → Repair API: COMMIT ok (reply)
  4. Repair API → Browser: 201 Created: R17 (reply)
  5. Next poll, 0.7 s later
  6. Notification worker → Repair DB: Claim a job for 5 min (call)
  7. Repair DB → Notification worker: Job J9 (reply)
  8. Notification worker → Email provider: POST /emails (call)
  9. Email provider → Notification worker: 202 Accepted (reply)
  10. Notification worker → Repair DB: Mark J9 sent (call)
  11. Repair DB → Notification worker: OK (reply)
Mermaid source
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    participant DB as Repair DB
    participant Worker as Notification worker
    participant Provider as Email provider
    UI->>API: POST /repairs
    API->>DB: INSERT request + job J9
    DB-->>API: COMMIT ok
    API-->>UI: 201 Created: R17
    Note over Worker: Next poll, 0.7 s later
    Worker->>DB: Claim a job for 5 min
    DB-->>Worker: Job J9
    Worker->>Provider: POST /emails
    Provider-->>Worker: 202 Accepted
    Worker->>DB: Mark J9 sent
    DB-->>Worker: OK

The browser has R17 after about 70 ms. The worker claims J9 on its next poll, 0.7 seconds later, and the email goes out after that.

To claim J9, the worker marks it as taken for five minutes. Any lock the database holds on the job ends when the claim commits, long before the email goes out, so this lease is what keeps other workers off J9 while this one works. If the worker dies, the lease runs out and J9 comes free again.

The table that holds J9 is a transactional outbox: a table where a service writes the work it wants done, in the same database transaction as the change that caused it. The booking and the job are committed together, or neither is. So there’s never a booking without its email job, or an email job for a booking that failed.

The notification worker is a background worker: it runs outside any request, so nobody waits for it. It polls, asking the database for the next job about once a second, and the database never calls it.

Moving the email out of the request doesn’t remove its failures. It moves them to the worker, where nobody is waiting to notice. A provider that is down, a reply that never comes and a worker that crashes after the email went out each need a drawing of their own. The companion article, Failure, asynchrony and races in sequence diagrams, draws them.

Actors, notes, boxes and short-lived participants

An actor is a participant that is a person. UML and Mermaid draw it as a stick figure. Use one when the interaction starts with someone’s action, such as Mina submitting the booking form.

A self-message is an arrow from a lifeline back to itself. It shows a step a participant takes on its own, such as the API checking a postcode. Draw one only when the reader needs that step.

A note holds anything that isn’t a message: a timeout, a wait, an assumption. Mermaid places a note over one participant or across two, or to the left of or right of one.

Some participants exist for only part of an interaction. Mermaid writes create participant on the line before the message that creates one, and destroy on the line before the message that ends it. Only a message’s receiver can be created. Either end of a message can be destroyed, which is why the listing destroys the quote renderer on the line before its reply, the last message it sends.

sequenceDiagram
    actor Mina
    participant UI as Browser
    participant API as Repair API
    Mina->>UI: Submit the booking form
    UI->>API: POST /repairs
    API->>API: Check postcode
    create participant Quote as Quote renderer
    API->>Quote: Render quote for R17
    destroy Quote
    Quote-->>API: quote.pdf
    API-->>UI: 201 Created: R17
    Note right of API: The renderer exists only for this request

A participant can also carry a symbol that says what it is. PlantUML has keywords for database, queue, boundary, control, entity and collections, and Mermaid 11.11 and later sets them with participant Q@{ "type" : "queue" }. A symbol only helps the reader recognise the participant. Like the queue shape in the whiteboard shape vocabulary, it says nothing about how a queue delivers.

A box groups participants: by service, by owner, or by which side of a trust boundary they sit on. Mermaid writes box LightBlue Our systems before the participants and end after them. PlantUML writes box "Our systems" #LightBlue and end box. A boundary can mean ownership, execution location or trust, so write which one you mean, as the layout article advises for any boundary. Trust boundaries reads a design with boxes drawn this way.

Flows with no caller

Some flows start with no caller in the diagram. The worker’s poll starts with a timer, and a bounce report arrives from another company’s system. Three drawings fit, depending on how much the trigger matters.

When the trigger is incidental, as the worker’s poll timer is, put a note on the lifeline, such as “Poll timer fires”, and start from there. In Mermaid, activate after the note starts the worker’s bar without a message.

When the trigger comes from outside what you are drawing and its sender doesn’t matter, draw a found message: one that arrives from a sender the diagram doesn’t show. UML starts its arrow from a filled circle, and PlantUML writes [-> Worker : Poll timer fires. Mermaid has no form for it, so use a note or a participant that stands for the outside world.

When the timer’s own behaviour is part of the question, because it can be cancelled, rescheduled or fire late, give the clock a lifeline of its own, a scheduler. Its messages are then ordinary arrows that can be drawn, counted and lost like any other.

How much to draw

Each diagram should answer one question. “What does Mina wait for?” and “What happens to the email when the provider is down?” are two questions. They share the API and the database, and little else. A diagram that tries to answer both ends up too crowded to answer either.

Choosing what to draw

Choose the question first, then draw only the lifelines and messages that change its answer. A gateway that forwards every request unchanged, a token check that always passes, logging, a cache that only saves time and a client library’s internal retries can all stay off the page. A question about gateway timeouts needs the gateway.

A message broker between two services is the same choice. Give the queue a lifeline when its acknowledgement, its keeping of messages or its redelivery affects the answer. Otherwise draw one asynchronous arrow from sender to consumer and name the queue in its label, such as Send J9 (email queue). Don’t draw both for the same hop, or the reader will count two messages.

Keep to one level of detail. Lifelines can be services, or they can be objects inside one service, but a diagram that mixes the two says little about either. Take the participants’ names from the architecture diagram at that level, so the two views can be read together. A diagram with a lifeline per class goes stale fastest, because every refactor changes it. Generate it from the code or don’t draw it. C4, a set of zoom levels for architecture diagrams, makes the same point about its code level, as the architecture article explains.

Draw the happy path first, the run where everything works, then one diagram for each failure that matters. The alternative is one diagram with an alt frame, a box whose sections hold alternative paths, around every failure. Combined fragments explains frames. The companion takes the first approach: each failure has its own figure.

Sketch in the order the reader will read. Place the participants first, from whoever starts the interaction on the left. Then draw the main path, with every reply that carries something. Add the frames and notes for variations last. The companion’s failure walk then tests the finished happy path.

Figure 5 shows the same facts twice. Start with Everything in one, then switch to the two focused diagrams.

Figure 5

One crowded diagram, or two

BrowserAPIgatewayAuthserviceRepair APIRepair DBWorkerEmailprovideralt[token valid][token invalid]POST /repairsCheck tokenOKPOST /repairsINSERT request + job J9COMMIT ok201 Created: R17201 Created: R17Invalid401 UnauthorizedClaim a job for 5 minJob J9POST /emails202 AcceptedMark J9 sent

One diagram holding the gateway, the auth check, the booking and the worker’s email: seven lifelines, an alt for a bad token, and thirteen messages on the valid path. The same facts appear as two focused diagrams under Booking only and Email only.

Messages in order, as text
  1. Browser → API gateway: POST /repairs (call)
  2. API gateway → Auth service: Check token (call)
  3. alt fragment, section [token valid] runs; next section [token invalid] is skipped
  4. Auth service → API gateway: OK (reply)
  5. API gateway → Repair API: POST /repairs (call)
  6. Repair API → Repair DB: INSERT request + job J9 (call)
  7. Repair DB → Repair API: COMMIT ok (reply)
  8. Repair API → API gateway: 201 Created: R17 (reply)
  9. API gateway → Browser: 201 Created: R17 (reply)
  10. Notification worker → Repair DB: Claim a job for 5 min (call)
  11. Repair DB → Notification worker: Job J9 (reply)
  12. Notification worker → Email provider: POST /emails (call)
  13. Email provider → Notification worker: 202 Accepted (reply)
  14. Notification worker → Repair DB: Mark J9 sent (call)

The single diagram has seven lifelines, an alt frame for a bad token and thirteen messages on the valid path. It holds two jobs with different timing, a request and a worker’s poll, so neither stands out. Each focused diagram fits a phone screen and answers its own question. A ref frame, which stands for another diagram, joins them. The gateway and the auth service dropped out because neither question depends on them.

Lifeline order, labels and numbering

Order the lifelines so that most arrows travel between neighbours. An arrow that skips over lifelines crosses their activation bars and is hard to follow. External systems usually go on the right, as the email provider does in the outbox diagram.

Keep labels short. In Mermaid a long label widens the whole diagram, and <br/> breaks a label or a note over two lines.

Number the steps when the diagram will be discussed: autonumber puts a number on every arrow, so a reviewer can say “step 7 has no timeout”. Add notes sparingly: a note on every arrow turns the diagram into prose. rect rgb(240, 240, 240) shades a region, such as one transaction, but never let colour carry meaning alone. The layout article covers the general principles for labels, colour and splitting that dense drawings share.

Ordering and gaps

Reading down a lifeline gives a firm order. So does each message: it is sent before it is received. Those two rules are all a sequence diagram promises. Two events on different lifelines that no chain of messages connects have no order between them, even though one is drawn higher on the page.

Take the outbox diagram. The API sends 201 Created after the commit, and the worker claims J9 after the commit too, but nothing links the two. Figure 6 redraws the start of Figure 4 with one change: the worker polls just after the commit instead of 0.7 seconds later. Watch when the browser gets R17. Then switch the poll back to 0.7 s later and watch which rows move.

Figure 6

The same messages in another order

BrowserRepair APIRepair DBNotificationworkerEmail providerPolls just after the commitPOST /repairsINSERT request + job J9COMMIT okClaim a job for 5 minJob J9201 Created: R17POST /emails

The browser posts the booking, and the API commits R17 with job J9. The worker polls just after the commit, so its claim and the reply Job J9 are drawn before the API’s 201 Created reaches the browser. Then the worker asks the provider to send the email. Each lifeline’s own messages come in the same order whichever way it is drawn; only the 201 and the worker’s poll trade rows.

Messages in order, as text
  1. Browser → Repair API: POST /repairs (call)
  2. Repair API → Repair DB: INSERT request + job J9 (call)
  3. Repair DB → Repair API: COMMIT ok (reply)
  4. Note on Notification worker: Polls just after the commit
  5. Notification worker → Repair DB: Claim a job for 5 min (call)
  6. Repair DB → Notification worker: Job J9 (reply)
  7. Repair API → Browser: 201 Created: R17 (reply)
  8. Notification worker → Email provider: POST /emails (call)
Mermaid source, for the early poll
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    participant DB as Repair DB
    participant Worker as Notification worker
    participant Provider as Email provider
    UI->>API: POST /repairs
    API->>DB: INSERT request + job J9
    DB-->>API: COMMIT ok
    Note over Worker: Polls just after the commit
    Worker->>DB: Claim a job for 5 min
    DB-->>Worker: Job J9
    API-->>UI: 201 Created: R17
    Worker->>Provider: POST /emails

With the early poll, the worker claims J9 before the browser has R17, and it could send the email before then too. Switching the poll moves only two things, the 201 and the worker’s poll, which trade rows. Every lifeline reads the same from top to bottom in both drawings, and every message is still sent before it is received.

The real timing differs between the two settings, but a sequence diagram doesn’t record timing, so the two drawings describe the same interaction. Figure 4 drew the 201 first, but nothing in the diagram promises that order. Here the order doesn’t matter, because R17 is stored from the commit onward: if the email reaches Mina before her browser shows R17, the booking it confirms already exists.

When a design does depend on one of these orders, the diagram needs a message that makes it so, or a note that admits nothing guarantees it. This is also why par exists: it says openly that the order between its sections is open. The rule is the one Leslie Lamport used for his space-time diagrams of distributed systems in 1978: one event comes before another only if the two are in order on one lifeline or a chain of messages leads from the first to the second.

As Figure 2 showed, a diagram has no scale, so a gap of two days looks the same as a gap of two milliseconds. Mark a long gap in words, such as a note across every lifeline: “Two days later”. Write deadlines and durations in notes too, such as “reply within 2 s”, rather than in UML’s formal duration constraints, which few readers know. When time in transit is itself the point, some tools can slant an arrow so that its arrival sits visibly lower than its sending.

Messages can also overtake each other: two sent in one order and delivered in the other. The companion’s Races and overtaking draws it.

Combined fragments

Some messages happen only sometimes, some repeat, and some run side by side. A combined fragment is a frame drawn around those messages that says which.

alt and opt

The first two kinds of frame choose which messages run. In the next animation the API checks Mina’s details before it stores anything, and a frame around the rest of the booking holds what happens in each case. Watch which part of the frame runs. Then set Details to Invalid.

Figure 7

Checking Mina’s details

BrowserRepair APIRepair DBalt[details valid][details invalid]opt[new customer]One transaction, ending at COMMITPOST /repairsINSERT customerOKINSERT repair requestCOMMIT ok201 Created: R17422: postcode missing

After POST /repairs an alt fragment offers two paths. The details are valid, so the first section of the alt runs. Mina is a returning customer, so the opt fragment is skipped and the API inserts only the repair request. The browser gets R17. The path not taken stays drawn, faded.

Messages in order, as text
  1. Browser → Repair API: POST /repairs (call)
  2. alt fragment, section [details valid] runs; next section [details invalid] is skipped; Note on Repair API and Repair DB: One transaction, ending at COMMIT
  3. opt fragment, section [new customer] is skipped
  4. Repair API → Repair DB: INSERT repair request (call)
  5. Repair DB → Repair API: COMMIT ok (reply)
  6. Repair API → Browser: 201 Created: R17 (reply)
Mermaid source
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    participant DB as Repair DB
    UI->>API: POST /repairs
    alt details valid
        Note over API,DB: One transaction, ending at COMMIT
        opt new customer
            API->>DB: INSERT customer
            DB-->>API: OK
        end
        API->>DB: INSERT repair request
        DB-->>API: COMMIT ok
        API-->>UI: 201 Created: R17
    else details invalid
        API-->>UI: 422: postcode missing
    end

The tag in the frame’s top-left corner, alt, is the operator, which names the kind of frame. An alt frame holds alternatives. The dashed line divides it into sections, and the condition in square brackets at the top of each, such as [details valid], is that section’s guard. It says when the section runs.

At most one section of an alt runs, one whose guard is true. A guard of [else] is true when no other guard is. The section that doesn’t run stays on the page, faded here, because the frame describes both paths.

The valid section holds a smaller frame, an opt fragment. It has one section, which runs only if its guard is true. Here the guard is [new customer], and Mina is a returning customer, so the API skips the fragment and inserts only the repair request. Set Customer to New, and the API inserts a customer record first, in the same transaction as the request.

Write guards that the participant making the choice can check. The API can check “details valid” when the request arrives. It can’t check “email will arrive”, because nothing in the interaction has told it that yet.

loop

A loop fragment repeats the messages inside it while its guard holds. The diagram draws them once.

After booking, Mina’s page asks for the repair’s status every 2 seconds until a technician is scheduled. Watch the pass counter in the frame’s corner.

Figure 8

Polling in a loop

BrowserRepair APIRepair DBloop[status not scheduled]pass 3Wait 2 sGET /repairs/R17SELECT statusstatus: scheduled200: scheduled

A loop fragment drawn once runs 3 times. Each pass the browser asks for the status of R17, the API reads it from Repair DB, and the browser waits 2 seconds. On pass 3 the status is scheduled, but the browser still waits 2 seconds before the loop checks its guard and ends.

Messages in order, as text
  1. loop fragment [status not scheduled] begins, pass 1
  2. Browser → Repair API: GET /repairs/R17 (call)
  3. Repair API → Repair DB: SELECT status (call)
  4. Repair DB → Repair API: status: received (reply)
  5. Repair API → Browser: 200: received (reply)
  6. Note on Browser: Wait 2 s
  7. Loop pass 2 begins
  8. Browser → Repair API: GET /repairs/R17 (call)
  9. Repair API → Repair DB: SELECT status (call)
  10. Repair DB → Repair API: status: received (reply)
  11. Repair API → Browser: 200: received (reply)
  12. Note on Browser: Wait 2 s
  13. Loop pass 3 begins
  14. Browser → Repair API: GET /repairs/R17 (call)
  15. Repair API → Repair DB: SELECT status (call)
  16. Repair DB → Repair API: status: scheduled (reply)
  17. Repair API → Browser: 200: scheduled (reply)
  18. Note on Browser: Wait 2 s
Mermaid source
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    participant DB as Repair DB
    loop status not scheduled
        UI->>API: GET /repairs/R17
        API->>DB: SELECT status
        DB-->>API: status
        API-->>UI: 200: status
        Note over UI: Wait 2 s
    end

The same five steps run on every pass, and each pass adds about 2 seconds to the clock. The last wait is wasted. The page already has its answer, but the loop waits before it checks the guard again.

Now set Never. The guard never becomes false, and nothing in the frame sets a limit, so the page asks forever. Real code needs a cap, such as 30 attempts. A cap in the guard stops the endless loop, and a break stops the wasted wait. UML can write the cap in the tag, loop(1, 30) for at least once and at most 30 times. Mermaid has no bound syntax, so put the cap in the label.

Not every repetition needs a frame. The outbox diagram draws one pass of the worker’s poll. The polling repeats, but that is the worker’s business and not part of the booking, so a note such as “Polls about once a second” says it without a loop frame. Draw a loop when the repetition is part of the question, as Mina’s status checks are here.

par

A par fragment holds sections that run independently. Messages from different sections can interleave in any order. Within one section, the order still holds.

Once Mina has R17, the page loads the repair’s status and the free appointment slots together. Watch both requests leave before either reply returns. Then change which reply comes back first.

Figure 9

Two requests in parallel

BrowserRepair APIpar[repair status][free slots]Show the page once both replies are inGET /repairs/R17200: receivedGET /slots?repair=R17200: three slots

A par fragment with two sections. The browser asks for the repair status and for free appointment slots without waiting between them. Inside each section the request still comes before its reply. The slots reply arrives first. A note after the frame says the page appears only once both replies are in.

Messages in order, as text
  1. par fragment, section [repair status] begins
  2. and section [free slots] begins
  3. Browser → Repair API: GET /repairs/R17 (call)
  4. Browser → Repair API: GET /slots?repair=R17 (call)
  5. Repair API → Browser: 200: three slots (reply)
  6. Repair API → Browser: 200: received (reply)
  7. Note on Browser: Show the page once both replies are in
Mermaid source, when both replies arrive
sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    par repair status
        UI->>API: GET /repairs/R17
        API-->>UI: 200: received
    and free slots
        UI->>API: GET /slots?repair=R17
        API-->>UI: 200: three slots
    end
    Note over UI: Show the page once both replies are in

Either order is allowed, and one diagram covers both. par says only that neither section waits for the other. If one request needs the other’s reply, they aren’t parallel. Draw them in order.

Now set Status never arrives. The slots come back, but the status section never finishes. If the page shows nothing until both replies are in, Mina waits forever. Say in a note after the frame that the page needs both replies, as the figure does when both arrive, and give each request its own timeout.

break

A break fragment runs its messages in place of the rest of the enclosing fragment. Inside a loop, it leaves the loop. The polling loop can stop as soon as the status is scheduled, and the cap moves into the loop’s guard:

sequenceDiagram
    participant UI as Browser
    participant API as Repair API
    loop up to 30 times
        UI->>API: GET /repairs/R17
        API-->>UI: 200: status
        break status is scheduled
            Note over UI: Show the appointment
        end
        Note over UI: Wait 2 s
    end

Now the loop ends at the first reply that says scheduled, without waiting another 2 seconds, and it never runs more than 30 times. Outside a loop, break is a common way to draw a failure path that ends the interaction early.

critical

A critical fragment marks a region that other messages can’t interleave with. In UML, no other event on the lifelines it covers can fall between its first and last message. Service diagrams rarely need it, because few services handle one message at a time.

It is also the most misread operator. Engineers often draw it around a database transaction, but a transaction is about isolation and commit, and the fragment promises neither. Where you mean a transaction, a note says it more plainly, as the alt example did with “One transaction, ending at COMMIT”. Mermaid’s documentation gives critical a second reading, something that must happen, with option sections for when it fails. When you meet a critical frame with options, ask which reading its author meant.

ref

A ref fragment, which UML calls an interaction use, stands in for another sequence diagram. It is a frame tagged ref with the other diagram’s name inside, and it is the main way to split a diagram that has grown too big. The outbox diagram has two starting points, the browser’s request and the worker’s poll. Split it at the database, and the worker’s diagram can start with a ref to the booking, as Figure 5’s Email only does. Mermaid has no ref, so use a note across the lifelines instead:

sequenceDiagram
    participant DB as Repair DB
    participant Worker as Notification worker
    participant Provider as Email provider
    Note over DB,Worker: ref: Mina books a repair (writes job J9)
    Worker->>DB: Claim a job for 5 min
    DB-->>Worker: Job J9
    Worker->>Provider: POST /emails
    Provider-->>Worker: 202 Accepted

Rare operators and deep nesting

UML defines more operators than these: seq, strict, neg, assert, ignore and consider. They matter mostly in formal specifications. You may meet neg in a review, where it marks a sequence that must never happen.

Fragments also nest, and a few levels in they stop helping. When alt branches each hold a screenful of messages, or a loop sits inside a par inside an alt, the diagram holds several scenarios. Split it, as How much to draw describes. When the alts keep multiplying because one record can be in many conditions, the question has become which events each condition allows, and a state diagram answers it in one drawing.

Reading a design from its diagram

A sequence diagram is something you review: your own before you share it, someone else’s before you build from it. Four readings come up again and again, and each shows faster in the drawing than in the code. They leave the booking for a real protocol or another flow wherever that shows the shape better. The failure questions, which walk each arrow and ask what happens if it is lost, late or doubled, are gathered in the companion’s failure walk.

Round trips

A round trip is a message and the reply its sender waits for. Each one on the way to a result costs at least the network delay there and back, however fast the work at either end. Count the round trips before the result, and you have a floor on its latency that no server tuning removes.

Protocol specifications draw these ladders themselves. Figure 10 is the start of a TLS connection, the encryption under HTTPS, drawn after the ladders in the TLS 1.2 and TLS 1.3 specifications, with each one-way trip taking 25 ms. Play TLS 1.2, then switch to TLS 1.3 and compare the clock when the request leaves.

Figure 10

Round trips before the first request

BrowserRepair APITwo round trips: 100 ms before the request can goClientHelloServerHello … ServerKeyExchange*, ServerHelloDoneClientKeyExchange, ChangeCipherSpec, FinishedChangeCipherSpec, FinishedGET /repairs/R17

A full TLS 1.2 handshake. The browser sends ClientHello and waits for the server’s flight: ServerHello, Certificate*, ServerKeyExchange* and ServerHelloDone. It then sends its key exchange and Finished and waits for the server’s Finished. Only after those two round trips, 100 ms at 50 ms each, can it send GET /repairs/R17.

Messages in order, as text
  1. Browser → Repair API: ClientHello (call)
  2. Repair API → Browser: ServerHello … ServerKeyExchange*, ServerHelloDone (reply)
  3. Browser → Repair API: ClientKeyExchange, ChangeCipherSpec, Finished (call)
  4. Repair API → Browser: ChangeCipherSpec, Finished (reply)
  5. Note on Browser: Two round trips: 100 ms before the request can go
  6. Browser → Repair API: GET /repairs/R17 (call)

A full TLS 1.2 handshake needs two round trips before the browser can send its request, 100 ms here. TLS 1.3 needs one. The browser guesses which key exchange the server will accept and sends its share in the first message, so the server can finish its side in one reply. The request then travels with the browser’s Finished. The TCP connection underneath adds one more round trip before either. Resumed sessions, and TLS 1.2 with the False Start extension, can send sooner. The figure counts a first, full connection.

The labels follow the legend in RFC 8446, the TLS 1.3 specification. A + marks an extension carried in the message before it, and * a message that isn’t always sent. Braces mark messages encrypted with handshake keys, and square brackets messages encrypted with application keys. Every specification defines its own marks, so read a ladder’s legend before its arrows.

The same count works inside your own system. In Figure 2 the browser waited for the API, which waited for the database. That is a chain of round trips, each nested in the one above, and each adds its delay to Mina’s wait.

Fork and stair

A drawing also shows who holds the flow. Figure 11 draws a booking that needs a time slot, a part and a deposit, in two shapes. Start with Central, then switch to Delegated.

Figure 11

The same booking, fork or stair

Repair APISchedulingInventoryBillingHold time for A5HeldReserve part P3ReservedTake depositPaid

Central control. The Repair API calls Scheduling to hold the time, then Inventory to reserve the part, then Billing to take the deposit, and hears each reply itself. The arrows fan out from one lifeline like the tines of a fork.

Messages in order, as text
  1. Repair API → Scheduling: Hold time for A5 (call)
  2. Scheduling → Repair API: Held (reply)
  3. Repair API → Inventory: Reserve part P3 (call)
  4. Inventory → Repair API: Reserved (reply)
  5. Repair API → Billing: Take deposit (call)
  6. Billing → Repair API: Paid (reply)

With central control, one lifeline sends every request and hears every reply, and the arrows fan out from it like a fork. With delegated control, each service does its part and hands the rest on, and the arrows step down and back like a stair. The IBM Rational Unified Process guidelines, which describe the two shapes, suggest the fork when the steps may change order or new steps are likely, because one place holds the order. The stair suits steps that are tightly bound and always run in the same order.

The stair has a cost the activation bars make visible. Every caller waits for everything below it, so Scheduling is held up by Billing, and a slow deposit holds three services. A failure at the bottom has to travel back up through every caller. When the steps talk through events rather than calls, the same two shapes are called orchestration and choreography, and the companion’s Failing part-way draws one.

Chatty calls

An interface is chatty when one result takes many small calls across the network, and a diagram shows two shapes of it. Figure 12 draws the first, a page that lists today’s repairs. The API reads them from the database, then needs each repair’s customer from another service. Play the default, then switch to one batched call.

Figure 12

One call per repair

BrowserRepair APIRepair DBCustomer serviceloop[each of 10 repairs]pass 10GET /repairs?day=todaySELECT today’s repairs10 rowsGET /customers/{id}200: customer200: 10 repairs

The browser asks for today’s repairs. The API reads 10 rows from Repair DB, then calls Customer service once for each repair inside a loop, ten round trips one after another, before it answers. The loop is drawn once and runs ten times.

Messages in order, as text
  1. Browser → Repair API: GET /repairs?day=today (call)
  2. Repair API → Repair DB: SELECT today’s repairs (call)
  3. Repair DB → Repair API: 10 rows (reply)
  4. loop fragment [each of 10 repairs] begins, pass 1; Repair API → Customer service: GET /customers/{id} (call)
  5. Customer service → Repair API: 200: customer (reply)
  6. Loop pass 2 begins
  7. Repair API → Customer service: GET /customers/{id} (call)
  8. Customer service → Repair API: 200: customer (reply)
  9. Loop pass 3 begins
  10. Repair API → Customer service: GET /customers/{id} (call)
  11. Customer service → Repair API: 200: customer (reply)
  12. Loop pass 4 begins
  13. Repair API → Customer service: GET /customers/{id} (call)
  14. Customer service → Repair API: 200: customer (reply)
  15. Loop pass 5 begins
  16. Repair API → Customer service: GET /customers/{id} (call)
  17. Customer service → Repair API: 200: customer (reply)
  18. Loop pass 6 begins
  19. Repair API → Customer service: GET /customers/{id} (call)
  20. Customer service → Repair API: 200: customer (reply)
  21. Loop pass 7 begins
  22. Repair API → Customer service: GET /customers/{id} (call)
  23. Customer service → Repair API: 200: customer (reply)
  24. Loop pass 8 begins
  25. Repair API → Customer service: GET /customers/{id} (call)
  26. Customer service → Repair API: 200: customer (reply)
  27. Loop pass 9 begins
  28. Repair API → Customer service: GET /customers/{id} (call)
  29. Customer service → Repair API: 200: customer (reply)
  30. Loop pass 10 begins
  31. Repair API → Customer service: GET /customers/{id} (call)
  32. Customer service → Repair API: 200: customer (reply)
  33. Repair API → Browser: 200: 10 repairs (reply)

A loop around a remote call on the request path is the shape to look for. The diagram draws the call once, so the loop’s guard is the only sign of how many times it runs. Here ten calls of 20 ms each, one after another, add 200 ms that one batched call cuts to 20. Inside a data layer the same shape is called the N+1 query problem, one query for the list and one per row, which The database work behind an application request follows through an ORM.

The second shape is a lifeline that every message passes through, such as a gateway or a central service that relays each call. It shows as a column with an arrow on almost every row. Every request then waits on it, and when it fails, it fails for everyone. That may be the right design, but the diagram should make it a visible decision rather than one the reader has to discover.

Trust boundaries

When boxes mark trust boundaries, the diagram answers which messages cross them. Figure 13 is Mina signing in to the repair app through a sign-in provider, using OpenID Connect, which runs on OAuth 2.0’s authorization code flow. Each party’s systems sit in a box of their own.

Figure 13

What crosses the browser when Mina signs in

Mina’s deviceRepair serviceProviderMina’s browserRepair appserverSign-in providerMina signs inGET /sign-in302 to provider + code_challengeGET /authorize302 to app with codeGET /callback?code=…POST /token: code, secret, code_verifierID token + access tokenSigned in (session cookie)

Three boxes: Mina’s device holds the browser, the repair service holds its app server, and the provider holds its sign-in server. The browser is redirected to the provider with a code_challenge, Mina signs in at the provider, and the provider redirects her back to the app with a code, so the code passes through the browser. The app then sends the code, its client secret and the matching code_verifier straight to the provider and gets an ID token and an access token back. The tokens never enter Mina’s device.

Messages in order, as text
  1. Mina’s browser → Repair app server: GET /sign-in (call)
  2. Repair app server → Mina’s browser: 302 to provider + code_challenge (reply)
  3. Mina’s browser → Sign-in provider: GET /authorize (call)
  4. Note on Sign-in provider: Mina signs in
  5. Sign-in provider → Mina’s browser: 302 to app with code (reply)
  6. Mina’s browser → Repair app server: GET /callback?code=… (call)
  7. Repair app server → Sign-in provider: POST /token: code, secret, code_verifier (call)
  8. Sign-in provider → Repair app server: ID token + access token (reply)
  9. Repair app server → Mina’s browser: Signed in (session cookie) (reply)

Follow what crosses into Mina’s device. The code returns to the app by way of her browser, in a redirect, so it can turn up in browser history, logs and anything else that sees the URL. The tokens travel straight from the provider to the app server and never enter her device. That is why the specification requires the code to expire shortly after it is issued, recommending ten minutes at most, and to be used only once.

The app server proves who it is with its client secret, but the secret says which app is asking, not which browser started the sign-in. So the current security guidance, RFC 9700, also ties the code to that browser with PKCE. The first redirect carries a code_challenge, a hash of a random value the app server keeps for this sign-in, and the token request must send the matching code_verifier. A code copied from a URL is useless without it.

Review a design this way by asking, for each arrow that crosses a box, what it carries and who could read or replay it. A diagram without boxes can’t answer the question at all.

Other dialects

You will read more of other people’s diagrams than you draw, and many don’t follow UML. A few dialects are common.

Text tools differ in a few marks too. Mermaid’s documentation describes its arrows by their shape and calls only -) and its dotted form --) asynchronous. Reading ->> as a call is the UML convention this article uses. The table gives the common marks in UML, Mermaid and PlantUML.

MeaningUML drawingMermaidPlantUML
A call: the sender waitsSolid line, filled head->>->
A replyDashed line, open head-->>-->
An asynchronous message: the sender carries onSolid line, open head-)->>
A lost messageEnds in a filled circleNone. -x draws a cross, used for this by convention->x, which PlantUML calls a lost message (a cross)
A found message, from outside the diagramStarts from a filled circleNone[->, or [o-> for the circle
An activation barA narrow bar on the lifeline+ and - after the arrow, or activate and deactivate++ and -- after the message, or activate and deactivate
A reference to another diagramA frame tagged refNone. Use a note across the lifelinesref over A, B : name
A long gapNone. Write it in a noteNone. Use a note across every lifeline...Two days later...
A slanted arrow for time in transitA sloped message lineNoneA ->(20) B

The same characters mean different things in the two tools. Mermaid’s -> is a line with no arrowhead and its ->> a call, while PlantUML’s -> is a call and its ->> an asynchronous message. Check which tool a listing is written for before reading its arrows.

Writing a diagram as text

Written as text, a diagram can live next to the code and change in the same review. Mermaid is the most widely rendered text form: GitHub and GitLab draw it from a fenced block, and the key under Figure 1 shows how to read it. Every listing in this article, including those under Mermaid source, is complete Mermaid and parses on Mermaid 11.4.1 and 12.2.0.

Mermaid can’t draw a ref frame, a found message, a delay marker or a slanted arrow. PlantUML can, with the marks in the table above. Each tool’s documentation is the reference for the rest, and Keeping diagrams useful helps choose a tool and pin its version.

A sequence diagram goes stale when a call is added or removed, a timeout or retry policy changes, or work moves between synchronous and asynchronous. Renaming a function doesn’t. Keep the source next to the code that implements the flow. When a reviewer doubts the diagram, compare it with a trace of the real flow, as the companion’s Drawing what happened describes.

Sources and further reading