An example of a sequence diagram is a small cast and the messages between them, drawn in time order, including what comes back. Below are three you can paste: a login, a single API request, and a checkout payment. Each one is explained as a picture of a conversation, not as a syntax lesson.
The sequence tutorial is where the arrows, notes, and numbering get the long treatment. I will use the arrows correctly here and I will not reprint that tutorial. If a keyword is unfamiliar, go there. If you want to see what “done” looks like for three ordinary features, stay.
How to read any of these without knowing the author
Time runs downward. The boxes across the top are participants, and the left-to-right order is the order they were declared, so I declare them in call order. A solid arrow is a request. A dotted arrow is a reply. I stick to that even when the diagram is tiny, because a four-message chart is where the habit either forms or doesn’t.
autonumber is off in these examples. Numbers help in a pull request (“step 4 is the lie”). They also clutter a gallery. Turn them on when you’re reviewing a specific change. Leave them off when you’re teaching the shape.
An alt block is a fork in the replies. It is not a flowchart diamond pasted on top of lifelines. The question lives in the alt / else labels. If you can’t name the else, you don’t have a branch, you have a wish. I close every alt with end. That word is syntax. It is not a participant, and naming a participant end will ruin your day.
Participants get aliases when I want a short id and a readable label. participant API as API looks redundant and stays out of the way. I avoid spaces in the id. The label can be boring. Boring is a feature. “Backend” is not a participant if three services could answer to it.
Login, including the bad password
A login diagram that only shows the happy path is how “invalid password” becomes a toast nobody specified. I want the 401 in the picture. I also want the database reply to be a reply, not a second request the API fires into the void.
sequenceDiagram
participant Browser as Browser
participant API as API
participant DB as DB
Browser->>API: POST /login
API->>DB: Find user by email
alt user and password ok
DB-->>API: User row
API-->>Browser: 200 and session
else no
DB-->>API: No row or mismatch
API-->>Browser: 401
endBrowser calls API. API calls DB. DB answers API. API answers Browser. The else covers two real cases, missing user and bad password, and I collapsed them on purpose. If your API returns a different body so an attacker can learn which emails exist, don’t collapse them, and don’t draw the helpful distinction either. Draw the response you actually send. I have reviewed a diagram that said “404 user” and “401 password” as two thoughtful branches. The author thought they were being precise. They were specifying an account oracle. The diagram was the spec, because the ticket linked it. We shipped the oracle. The fix was to make the else one 401 and to delete the clever branch.
Notice the browser never receives the row. The row stops at the API. If your diagram has DB-->>Browser, you’ve drawn a connection the browser shouldn’t have, and someone will ask why the client SDK is talking to the database. Answer: it isn’t, the chart was sloppy. Fix the arrow’s target before the review wanders into architecture.
The user-story sequence post is the right follow-up when your source is “as a user I want to log in” and you need to pull participants out of that sentence. A user story will not mention the database. You add the database only if the API really reads one. A login that calls a hosted identity provider is a different cast: Browser, App, IdP. Don’t keep DB in the picture because this example had one.
One API request that can miss
Login hides inside a product. This next picture is the request itself, the thing a client developer actually integrates. GET /invoices/123. Either the invoice is theirs, or it isn’t, or it isn’t there. I draw two failures because they are different messages, and I have seen them implemented as one “error” toast that sent people retrying a 404.
sequenceDiagram
participant Client as Client
participant API as API
participant DB as DB
Client->>API: GET /invoices/123
API->>DB: Load invoice
alt found and owned
DB-->>API: Invoice
API-->>Client: 200 body
else missing
DB-->>API: Empty
API-->>Client: 404
else not owned
DB-->>API: Invoice
API-->>Client: 404
endThe not-owned branch still loads a row and still returns 404. That’s a policy choice. Some APIs return 403, which tells the caller the id exists. I don’t want that on an invoice. I return 404 for both missing and forbidden, and the diagram says so by giving both elses the same status with different reasons on the DB side. If you “simplify” the diagram down to one else, the implementer will pick 403 because 403 feels more correct in a textbook. Textbooks don’t get the abuse mail.
Is the second else a lie about the database? It depends. If ownership is a WHERE user_id = ? clause, the database returns empty for both failures and you cannot honestly draw “Invoice” on the not-owned branch. In that design, delete the third branch and write one else: empty becomes 404. I included both because our older service loaded by primary key and checked the owner in the app, which is its own bug farm, and the diagram was how we noticed the row crossed the process boundary before the check. Draw the system you have when you’re debugging. Draw the system you want when you’re proposing the fix. Label which one it is in the sentence above the fence. I forgot to label one once. A teammate “fixed” production to match a proposal. That was a long Friday.
The loop article is what you read next if this GET is actually a poll. Don’t add a loop here to look thorough. A loop says the client repeats. This client doesn’t. One request, one response.
Checkout payment, with the bank in the cast
Payment is where sequence diagrams earn the ink. The shop must not mark the order paid because the browser hoped. The pay service must not invent an approval the bank didn’t send. Three participants is the minimum cast I trust. Two participants (“App” and “System”) is how the bank disappears and the bug returns.
sequenceDiagram
participant Shop as Shop
participant Pay as Pay
participant Bank as Bank
Shop->>Pay: Charge card
Pay->>Bank: Authorize
alt approved
Bank-->>Pay: OK
Pay-->>Shop: Paid
Shop->>Shop: Mark order paid
else declined
Bank-->>Pay: No
Pay-->>Shop: Failed
Shop->>Shop: Keep order unpaid
endShop calls Pay. Pay calls Bank. The approval comes back down the same stack. Only then does Shop mark the order. The self-message Shop->>Shop is a little ugly and I keep it. It makes the state change a step that happens after the reply, in the shop, not inside the bank. If you omit it, readers think “Paid” means the order row changed, and they will look for that write inside Pay. It isn’t there. Pay doesn’t own the order. I want the picture to be stubborn about that.
The decline path does not mark the order paid and does not delete the order. “Keep order unpaid” is a boring self-message. Boring is the point. A decline that falls off the diagram looks like the order vanishes. It doesn’t. It sits there, unpaid, annoying a support person. Draw the sit.
I left out retries, 3-D Secure, and webhooks. Each of those is a real sequence in a real shop, and each of them is another diagram. Stuffing them into this alt produces a ladder nobody reads and a false sense that you’ve specified checkout. You specified the authorization reply. Say that next to the figure. The alt and else writeup goes further on branches, including the nested ones I’m glad I didn’t draw here. Nested alts are where a payment chart goes to die.
A self-message is easy to overuse. Shop->>Shop: Be correct is not a step. I only draw one when the state change would otherwise be invisible. If nothing changes on the decline except a message to the shopper, the reply Failed is enough and the self-message is clutter. I kept both because our bug was the row, not the toast.
What these three share, and what I refuse to add
Declaration order matches call order. Browser before API before DB. Client before API before DB. Shop before Pay before Bank. When someone “cleans up” the file and moves Bank to the top because banks feel important, the eye reads the story backwards. Don’t. Importance is not layout. Call order is layout.
Every request I care about has a reply in the same diagram. A charge with no bank reply is a flowchart that wandered into the wrong type. If the reply is asynchronous, draw the later message as its own arrow from Bank to Pay and say it’s async in the label, or split the diagram. A missing arrow is not “implied async.” It’s a hole. I have watched an implementer treat a missing return as permission to return immediately. The diagram allowed it by saying nothing.
I don’t add a participant for the shopper. The shopper clicks, and that click is the first message from Shop or Browser. A stick figure named User who sends “wants to buy” is a use case wearing sequence clothes. It adds a lifeline and no information. If you need the human, the sequence type page still won’t give you a stick figure, and you shouldn’t fake one with a participant called User unless User is a service. Name the software.
The OAuth sequence template is the example I reach for when login isn’t a password table. Don’t mash that template into the first diagram and keep both the DB and the identity provider “just in case.” One login design per picture. Two designs is how you implement both and call it a migration.
Gallery mistakes I keep red-penning
The message text is a paragraph. “POST /login with email and password and also the device id if we have it and don’t log the password” is a spec comment, not an arrow. The arrow says POST /login. The password-logging rule goes in the paragraph under the figure, where a reviewer can quote it. Long arrows push the lifelines apart until the reply is off screen. People then delete the reply to make it fit. You can guess which part was the bug.
Both branches return 200. I see this on payment diagrams when the author wants the frontend to “handle it.” The frontend cannot handle a lie. If the charge failed, the status is not the success status. Draw the failure status you want the client to branch on. The free sequence maker notes are relevant if you’re about to paste these into a tool and export. They’re not a reason to soften the 401 into a 200 with an error field “for consistency.” Consistency with the success path is how clients ignore failures.
Participants that never receive a message. Someone adds Queue because the platform has a queue. The queue has no arrow. Delete the box. A box with no messages is a logo. Sequence diagrams are a bad place for logos.
Using rect colors and notes to rescue a confusing cast. If you need a colored rectangle to explain that Pay and Bank are different companies, the labels have failed, or you have too many lifelines. Fix the cast. Six participants for a login means you drew the network diagram in the wrong type.
Copying the checkout alt into the login and keeping the words Paid and Failed. I have done the lazy version of that. The file parsed. The login “marked the order paid.” Examples are not templates until you change the nouns. Change the nouns first, then the arrows.
Paste one, then change the nouns
Take the login if your task is a session. Take the GET if your task is a resource the client might not own. Take the payment if money moves and a third party has to agree. Don’t take all three and concatenate them into “the system.” The system is not a sequence. A sequence is one conversation with a start you can point at.
If a branch is missing, add an else before you add a new participant. New participants are the expensive kind of edit. They change the reading order of the whole chart. A missing else is one block. The sequence generator can draft from a sentence when you name the participants and both outcomes. Give it the cast from one of these examples and your real route names. Don’t give it “diagram our auth” and then feel betrayed when it invents SSO, a risk engine, and a participant called System.
I still edit the reply by hand after a draft. The model likes every call to succeed. Your users don’t. The else is the part I check first, the way I check a test for the error path before I admire the happy one.
Open the editor and paste the payment ladder if you want to see the self-message. Then rename Shop, Pay, and Bank to the services in your repo. If a name doesn’t map, you don’t have that lifeline, and the example was never your architecture.
Related posts
Frequently asked questions
Are these UML sequence diagrams?
They use the same ideas: lifelines, messages, and a fork. The syntax is Mermaid, not a CASE tool's XML.
Why is the reply dotted?
So you can see the answer separately from the request. A chart of only solid arrows hides which way the information flowed.
Where is the tutorial?
The sequence diagram tutorial teaches the method. This page is three finished examples you can adapt.