Diagrams
SequenceFigure
A call chain over time: who asks whom, in what order, and what comes back.
Usage
When to reach for it
Ships from@misoto22/design/diagrams
import { SequenceFigure } from '@misoto22/design/diagrams'Notes
A call chain over time: who asks whom, in what order, and what comes back.
The one diagram type here whose vertical axis MEANS something. Every other figure's y is layout; a sequence's y is time, which is why a message carries an explicit y rather than an index — two calls 8 units apart happened together, and two 200 apart did not, and an evenly spaced list of messages would erase that distinction while looking tidier.
Lifelines are hairlines, not solid rules, because they are the axis rather than the content. A lifeline drawn at the weight of a message is a diagram where seven vertical lines compete with twelve horizontal ones.
Activation bars say who is BUSY, which is the fact a sequence diagram carries that a list of calls does not — the third participant's bar overlapping the second's is the reason to draw them at all.
Segments band the axis into phases — request, fallback, response — printed as a rule with a mono caption rather than as a tinted panel, for the same reason architecture boundaries are: a second ground inside the figure would sit under every message label's mask.
A return message is dashed AND takes an open arrowhead. Two signals rather than one, because the reply is the thing a reader most often needs to pick out of a dense trace, and a dash alone is doing the same work as dashed already does for an asynchronous call.
Anatomy
| Element | Description |
|---|---|
| Figure shellrequired | The frame around the picture. What a trace hands it is a message list, and it is published in the order the messages array is written — which is the only ordering a reader who cannot see the axis is given. |
| Participant headrequired | A 148-unit plate per participant at the top of its column, in the order the participants array puts them. meta.column_fit: "spread" widens every column to the widest name instead of holding the fixed width. |
| Lifelinerequired | A hairline dropped from each head to below the last thing on the axis. A hairline and not a rule, because it is the axis: seven lifelines at message weight is seven vertical lines competing with twelve horizontal ones. |
| Messagerequired | A horizontal line at its own y, inset from both lifelines so the arrowhead lands clear of the axis, with the wording on a mask above it. A return is dashed AND takes an open head — two signals, because the reply is what a reader picks out of a dense trace. |
| Activation bar | A narrow bar on a lifeline from one y to another: who is BUSY. It is the fact a sequence diagram carries that a list of calls does not, and one bar overlapping another is the reason to draw the picture at all. |
| Segment band | A dashed rule across the whole figure with a mono caption on a masked patch — request, fallback, response. A caption rather than a tinted panel, because a second ground inside the figure would sit under every message label’s mask. |
Best practices
Do
- Author the messages in ascending y. The picture reads the axis and the summary list beside it iterates the array, so a message written out of order is drawn at one point in the exchange and read aloud at another.
- Reach for meta.column_fit: "spread" when a participant’s name is longer than the fixed column. The plate wraps to two lines and ellipsises whatever still does not fit, and shortening the name to make it fit is not a repair — the name is the data.
- Space the messages by what actually happened. Nothing normalises y, so eight units between two calls reads as together and two hundred reads as a wait; an evenly spaced list is tidier and says something the trace does not.
Don’t
- A message naming a participant that participants does not declare leaves the picture and the summary list together — there is no column to draw it between, and a text equivalent reporting a call the picture cannot show is two halves of one figure disagreeing. Development prints DIAGRAM_EDGE_DANGLING with both ends, which is now the only place that message is reported at all.
- This figure ASSERTS order. Three calls a service makes concurrently, given three y values because they had to be given something, are a picture claiming the second waited for the first — an activation bar or a segment caption is where "these overlap" belongs.
Examples
default
A cache miss read down the page, where the vertical axis is time rather than layout: the 50 units between the miss and the query are a real gap, and an evenly spaced list of six calls would have erased it while looking tidier. The activation bars carry the fact a list of calls cannot — Redis is busy for 45 units inside the 210 the API spends open. Returns are dashed and take an open head, two signals rather than one, because the reply is what a reader picks out of a dense trace first.
A cache miss
4 elements and 6 relationships. The vertical axis is time, not layout.
- Web app (frontend) — React
- API (backend) — handler
- Redis (database)
- Postgres (database)
- Web app → API: GET /me
- API → Redis: read
- Redis → API: miss
- API → Postgres: select
- Postgres → API: rows
- API → Web app: 200 JSON
- Phase Request covers GET /me, read, miss.
- Phase Fallback covers select, rows, 200 JSON.
- API is busy from GET /me to 200 JSON.
- Redis is busy from read to miss.
- Postgres is busy from select to rows.
an asynchronous hop
The asynchronous half of a delivery. The enqueue is dashed because Billing does not wait for it — Billing has no activation bar at all, which is what says the two attempts that follow cost the caller nothing. Both calls out carry the security variant, which the key reads as crossing a trust boundary, and it is true here: the endpoint belongs to the customer. column_fit is spread because Partner webhook endpoint does not fit the fixed 148-unit column, and shortening the name to make it fit would be a different diagram.
Webhook delivery
3 elements and 5 relationships. A queue absorbs the retry the caller never waits for.
- Billing (backend) — invoice writer
- Outbox (messagebus) — delivery queue
- Partner webhook endpoint (external) — customer-operated
- Billing → Outbox: enqueue
- Outbox → Partner webhook endpoint: POST /hooks
- Partner webhook endpoint → Outbox: 503
- Outbox → Partner webhook endpoint: attempt 2
- Partner webhook endpoint → Outbox: 200
- Phase Retry covers attempt 2, 200.
- Outbox is busy from enqueue to 200.
time as the axis
What an explicit y buys. The first three calls sit 12 units apart because they left the gateway together, and the vendor reply is 186 units below its call because that is how long it took — placed from an index instead, the six messages would space evenly and that difference would be gone. The activation bars carry the same fact a second way: two close early while the third runs to the bottom of the figure, which is the tail, visible before anyone reads a label.
A slow dependency
4 elements and 6 relationships. Three calls in 24 units, then 186 of waiting.
- Gateway (backend)
- Search (backend)
- Ranker (backend)
- Pricing vendor (external)
- Gateway → Search: query
- Gateway → Ranker: features
- Gateway → Pricing vendor: quote
- Search → Gateway: hits
- Ranker → Gateway: scores
- Pricing vendor → Gateway: price
- Gateway is busy from query to price.
- Search is busy from features to hits.
- Ranker is busy from quote to scores.
- Pricing vendor is busy from hits to price.
- Search and Ranker both answer within 75 units of the first call.
- The vendor holds its request open for 186.
names that do not fit
column_fit spread, which widens every column to the widest participant name instead of holding the fixed one. Reach for it the moment a real name would be clipped: the plate wraps to two lines and ellipsises the rest, and shortening the name to make it fit is not a repair — the name is the data, and dealer-portal-api is a different service from the api. A message naming a participant that participants does not declare is not drawn at all, because there is no column to draw it between; it stays in the summary list as the raw id, so the text reports a call the picture does not show.
Signing in to the dealer portal
3 elements and 4 relationships. Every column as wide as the longest real name.
- Dealer portal (browser) (frontend) — Next.js
- dealer-portal-api (backend) — Django + DRF
- SQL Server 2025 (database) — legacy schema
- Dealer portal (browser) → dealer-portal-api: POST /api/v1/token/
- dealer-portal-api → SQL Server 2025: SELECT dealer WHERE email = ?
- SQL Server 2025 → dealer-portal-api: one row
- dealer-portal-api → Dealer portal (browser): access + refresh
- dealer-portal-api is busy from SELECT dealer WHERE email = ? to access + refresh.
- SQL Server 2025 is busy from SELECT dealer WHERE email = ? to one row.
Accessibility
- Return messages are dashed AND take an open arrowhead — two signals, because the reply is what a reader most often needs to pick out of a dense trace.
- The message list beside the picture reads in order, which is the same order the axis is drawn in.