PlantUML state diagrams: four ways to model an order lifecycle

puml.online

State diagrams are the most underused PlantUML type. They precisely describe all legal states of a domain object (order, login session, approval flow), transitions, guards, and entry actions.

What state diagrams solve

Order lifecycle looks like this in practice:

  • pending → paid → shipping → delivered → completed
  • paid → refund-requested → refunded
  • pending → cancelled
  • pending → timeout
  • delivered → after-sale-requested → after-sale → after-sale-done

In code these become a sea of if/else/switch. State diagrams draw the lifecycle first; the code mirrors the diagram.

1. The most basic state diagram

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@startuml
title Order lifecycle (basic)

[*] --> PendingPayment : create
PendingPayment --> Cancelled : user cancel
PendingPayment --> Timeout : 24h unpaid
PendingPayment --> Paid : payment success

Paid --> AwaitingShipment : confirm
AwaitingShipment --> Shipped : warehouse pick
Shipped --> Delivered : user receive

Delivered --> Completed : 7 days later
Delivered --> AfterSale : request refund
Completed --> [*]
Cancelled --> [*]
Timeout --> [*]
Refunded --> [*]

AfterSale --> Refunded : approve
AfterSale --> AfterSaleDone : exchange only
AfterSaleDone --> [*]
Refunded --> [*]
@enduml
  • [*] is the start/end pseudo-state
  • StateA --> StateB : trigger event is the transition
  • Renders as rounded rectangles + black arrows + grey default fill

This covers 80% of domain objects.

2. Add guard conditions

Not every transition should fire. Use [] after the trigger:

1
2
3
4
5
6
7
8
9
10
@startuml
title Order with guards

[*] --> PendingPayment
PendingPayment --> Paid : user pays [signature valid]
PendingPayment --> Timeout : 24h timer [trigger timeout task]
PendingPayment --> Cancelled : user cancel [within 24h]

note right of PendingPayment : 24h+ orders must be altered by a cron task
@enduml

[condition] is the standard UML guard syntax.

3. Add entry actions and activities

What should run when entering a state (send email, reserve stock):

1
2
3
4
5
6
7
8
9
10
11
@startuml
title Order with entry actions

[*] --> PendingPayment
PendingPayment --> Paid : payment / reserve stock
Paid --> AwaitingShipment : enqueue / await scan
Shipped --> Delivered : user confirm / award points

note right of Paid : log + notify
note left of AwaitingShipment : warehouse consumes picklist
@enduml

Syntax: src --> dst : trigger / entry action. The slash + spaces matter.

4. Composite / nested states

“Shipping” is a process, not a single step. Nest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@startuml
title Order — composite state

[*] --> PendingPayment
PendingPayment --> Paid

Paid --> ShipInProgress

state ShipInProgress {
[*] --> AwaitingPick
AwaitingPick --> Picking : warehouse task start
Picking --> Packed : box complete
Packed --> AwaitingHandoff : awaiting carrier
AwaitingHandoff --> Shipped : pickup done
}

Shipped --> Delivered
Delivered --> [*]
@enduml

state X { ... } declares X’s sub-state machine. PlantUML renders a big rounded rectangle containing smaller ones.

5. History markers (H / H*)

When leaving and re-entering a composite state, do you return to the entry or to the last visited sub-state?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@startuml

state ShipInProgress {
[*] --> AwaitingPick
AwaitingPick --> Picking
Picking --> Packed
Packed --> AwaitingHandoff
AwaitingHandoff --> Shipped

AwaitingHandoff --> AwaitingPick : rollback [stock shortage]
}

note right of ShipInProgress : H markers track re-entry point
@enduml

PlantUML has limited support for H / H*; for serious history tracking, store the current sub-state in your data and re-enter with that target.

6. Concurrent regions (fork / join)

Multiple parallel things within one state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@startuml
title Order — concurrent processing

[*] --> OrderCreated
OrderCreated --> AwaitingAction
AwaitingAction --> ConfirmingPayment

state OrderFulfillment {
ConfirmingPayment --> ForkPoint
ForkPoint --> StockReservation
ForkPoint --> RiskCheck

StockReservation --> JoinPoint
RiskCheck --> JoinPoint
JoinPoint --> Confirmed
}

Confirmed --> [*]
@enduml

PlantUML supports fork / join via <<fork>> / <<join>> stereotypes but the simpler way is just two outgoing arrows from the same state:

1
2
PendingPayment --> RiskCheck : parallel -1
PendingPayment --> StockReservation : parallel -2

The reviewer reads parallel intent fine.

7. Time-triggered transitions

PlantUML doesn’t natively support after 24h. Workarounds:

1
2
3
4
5
6
@startuml
title Order with time triggers

[*] --> PendingPayment
PendingPayment --> Timeout : <&hourglass> 24h [cron task]
@enduml

<&hourglass> marks “triggered by an external scheduler.” Code side:

1
2
// The diagram describes WHAT should happen;
// cron code on the server is responsible for WHEN.

Real example: login session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@startuml
title Login session

[*] --> Anonymous
Anonymous --> LoggedIn : login OK / issue token
LoggedIn --> Anonymous : logout / revoke token

Anonymous --> AwaitingRefresh : expired token presented [request]
AwaitingRefresh --> LoggedIn : refresh OK
AwaitingRefresh --> Anonymous : refresh failed / require re-login

LoggedIn --> Locked : 5 fails / reset counter
Locked --> Anonymous : <&hourglass> 30 min / unlock

note right of Locked : all requests return 423
@enduml

This drives the code:

1
2
3
4
5
6
switch (session.state) {
case 'Anonymous': /* require login */ break;
case 'LoggedIn': /* check token TTL */ break;
case 'AwaitingRefresh': /* try refresh */ break;
case 'Locked': /* deny all */ break;
}

Review checklist

  • All start/end pseudo-states use [*]?
  • Guards ([...]) are after the trigger label?
  • Entry actions (/ action) defined and impl’d?
  • Composite state sub-diagram is independently readable?
  • Time-triggered transitions reference a cron / scheduler in code?
  • State graph covers all legal paths and exception paths?

Pitfalls

  • Long labels: Pending --> Timeout : user clicked cancel and gateway callback failed and the order is unconfirmed — PlantUML truncates. Move the detail to a note.
  • [*] as transition source: you must write [*] --> X : event explicitly.
  • Composite [*] --> everywhere: nested sub-state entry uses the parent state entry; don’t sprinkle [*] -->.
  • No brackets around guards: PlantUML treats the bare label as a trigger description, badly.
  • Time triggers ignored: after 24h doesn’t work in PlantUML; either use icons for a scheduler or move into code cron.

Comparison with UML standard

Feature UML Standard PlantUML
Simple state
Start / terminate
Trigger
Guard
Entry / exit actions ✅ (entry / exit)
Composite
History (H / H*) ⚠️ Partial
Deep history ⚠️ Partial
Fork / join
Time trigger after ❌ (icon + external cron)

PlantUML state diagrams load into UML tools (EA, StarUML) without translation.

TL;DR

State diagrams are PlantUML’s most underused type. Draw the lifecycle first, then code mirrors the diagram — no more spaghetti if/else.

  • Title: PlantUML state diagrams: four ways to model an order lifecycle
  • Author: puml.online
  • Created at : 2026-07-29 15:00:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-state-diagram-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.