Engineering Atlas › Low-Level Design

Drawing UML activity diagrams

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

The nodes

An order fulfilment process at a warehouse makes a good example. It starts at the initial node (a filled circle). Actions are rounded rectangles: Receive order, Charge card, Pick items, Ship. Control flows are arrows between them.

A decision (diamond, one input) picks exactly one outgoing edge, each labelled with a guard such as [approved] or [else]. A merge (diamond, several inputs) rejoins alternative paths without waiting. Both use the same shape, so label them.

A fork (thick bar, one input) starts concurrent flows: after Receive order, charging and picking run in parallel. A join (thick bar, several inputs) waits until every incoming flow has arrived.

There are two ends. The activity final (bullseye) stops the whole activity, including any flows still running elsewhere. The flow final (circle with an X) ends only the flow that reaches it. Using an activity final at the end of one branch of a fork silently kills the other branches, which is sometimes intended (cancel everything on fraud) and often not.

Tokens

The UML specification defines activity semantics as token flow. The initial node places a token on its outgoing edge. An action fires when a token arrives, and when it completes it offers a token on its outgoing edges. A decision routes its token down one edge; a fork copies it onto every edge; a merge passes each token through as it arrives; a join waits until it holds one token from each incoming edge, then emits one.

This is the Petri net model, where places hold tokens and transitions consume and produce them. Questions such as "can it deadlock?" or "can an action run twice for one order?" become questions about where tokens can end up.

Two bugs follow directly. If a decision inside one fork branch sends its token somewhere other than the join, the join waits forever: a deadlock. If two fork branches end at a merge instead of a join, the action after the merge runs twice.

Swimlanes, objects and signals

Partitions split the diagram into lanes, one per actor, team or service: Customer, Payments, Warehouse. Every arrow that crosses a lane is a handoff, and handoffs are where delays, lost messages and blame live.

Object nodes show data passed between actions, such as an Invoice. Send signal and accept event actions show asynchronous messages and waits, an hourglass shows a timer, and an interruptible region (dashed box with a lightning-bolt edge) cancels everything inside when an event such as "Order cancelled" arrives.

Choosing activity over sequence

Use an activity diagram for control flow: branches, loops, parallelism and who does what. Use a sequence diagram for which component calls which, in what order. A checkout often needs both.

Drawing it in Mermaid

Mermaid has no activity diagram type, so a flowchart stands in: diamonds {"..."} for decisions, labelled edges for guards, and plain nodes named "fork" and "join" for the bars. PlantUML's activity syntax has fork, fork again and end fork keywords, swimlanes with |Lane|, and kill and detach for the two finals.

Reviewing agent-written workflows

Agents generating workflow definitions or Promise.all code make the token bugs above: a decision in a parallel branch that skips the join, a merge where a join was needed, an early return that leaves other branches running. Ask where every token goes on every path, or run the model through an executor like the practice task's.

The same shape elsewhere

Fork and join are the fan-out shape: scatter, then gather under a policy (all, any, quorum). A search engine querying 50 shards and a quorum write waiting for two of three replicas face the same choices, and the join policy and straggler handling matter more than the fork.

Down the stack

Layer 1The contract

A well-formed activity promises: an action runs once per token it receives; a decision emits exactly one token, so its guards must cover every case (an [else] edge guarantees it); a join emits only after receiving a token on every incoming edge; an activity final terminates all flows immediately, and a flow final consumes one token and nothing else. The activity is complete when a token reaches an activity final, or when no tokens remain. If tokens remain and nothing can fire, the activity is deadlocked.

Layer 2The mechanism

Workflow engines implement this contract directly. BPMN 2.0, the process notation from the same standards body as UML, has an exclusive gateway (decision), a parallel gateway (fork when it has several outputs, join when it has several inputs) and an inclusive gateway (fire some outputs, then wait only for those). Camunda's Zeebe engine records, per process instance, which incoming sequence flows of a joining parallel gateway have been taken, and activates the gateway when all of them have.

AWS Step Functions models fork and join as a Parallel state with a Branches array. It starts every branch, waits for all of them, and passes on an array of their outputs; if any branch fails, the whole state fails and the other branches are stopped, unless a Catch handles it. Airflow expresses joins as trigger rules on a task: all_success (the default), all_done, one_success, none_failed. Temporal leaves it to code: start activities as futures, then wait on all of them.

In code, a Go fork is go f() and a join is sync.WaitGroup.Wait or errgroup.Group.Wait, which also cancels a shared context on the first error. Promise.all is a fail-fast join and Promise.allSettled waits for all. Java's ForkJoinPool adds work-stealing deques per worker thread.

Layer 3The machine

In-process forks are cheap. A goroutine starts with a 2 KB stack and is created in well under a microsecond; an OS thread reserves megabytes of virtual stack and takes tens of microseconds to create, which is why thread pools exist. A join on a WaitGroup is an atomic counter decrement per branch plus, for the waiter, a futex sleep and wakeup costing a few microseconds.

Durable engines persist every token move so the process survives crashes. Step Functions Standard workflows bill each state transition (on the order of $25 per million), Zeebe appends each move to a replicated log, and Temporal caps a workflow's event history, so long loops use continue-as-new. At 50 transitions per instance and 2 million instances a month, that is 100 million transitions, a real line item.

Why it's built this way

  1. Business processes branch, run in parallel and span teams, so the notation needs decisions, forks and lanes, not just sequence.
  2. Pictures of concurrency are easy to misread, so UML gives them precise token semantics borrowed from Petri nets.
  3. Precise semantics make the diagram executable, so BPMN engines and workflow services run the model itself.
  4. Long-running processes must survive crashes, so engines persist every token move, which makes each transition cost money and latency.

Diagram

flowchart TD
  I(("start")) --> R["Receive order"]
  R --> F["fork"]
  F --> C["Charge card"]
  F --> P["Pick items"]
  C --> D{"approved?"}
  D -- "[yes]" --> J["join"]
  D -- "[else]" --> X["Cancel order"]
  X --> AF(("activity final"))
  P --> J
  J --> S["Ship"]
  S --> E(("end"))

Worked example

A retailer runs order fulfilment on a workflow engine at 40,000 orders a day. The diagram in the design doc forks after Receive order into Charge card and Pick items, with a join before Ship. Declined cards go to Cancel order, which the team drew ending in a flow final, because "only the payment branch is cancelled".

About 3 percent of cards are declined, 1,200 orders a day. In each, the payment branch's token is consumed by the flow final and the picking branch's token waits at the join forever. The engine shows 1,200 new instances a day stuck "active". Each holds a picked tote on a shelf, and after a week 8,400 totes block the packing area. Nobody gets an error, because a waiting join is a legal state.

An activity final would stop the picking branch but could leave it half done. The team instead routed Cancel order into an interrupting event that also runs Return items to stock, and added a review rule: every fork branch must reach the join, an activity final, or a compensating interrupt. A token-flow executor in CI now runs each process definition under every guard combination, which takes milliseconds.

Trade-offs

OptionStrengthsWeaknessesChoose when
UML activity diagramReadable, precise token semanticsNot executable by itselfDesign discussion, documenting processes
BPMN on an engine (Camunda, Zeebe)The diagram is the program, auditableEngine to operate, per-transition costLong-running business processes with human steps
Step Functions or Airflow definitionsManaged, durable, retries built inJSON or Python DSL hides the pictureCloud workflows, data pipelines
Workflow as code (Temporal)Full language, testableDiagram must be drawn separatelyComplex logic, developer-owned processes
Plain code (errgroup, Promise.all)Fast, no infrastructureLost on crash, no audit trailShort in-process concurrency

Pitfalls in production

Leadership lens

Check yourself

  1. Two parallel branches end at a diamond with two inputs and one output. Is that a merge or a join, and what goes wrong?

    Show answer

    A diamond is a merge, so it passes both tokens through and the next action runs twice. A join bar is needed to wait for both and emit one token.

  2. Why does a flow final in one branch of a fork usually cause a deadlock?

    Show answer

    It consumes that branch's token, so the join never receives a token on that edge and waits forever.

  3. How does an activity final differ from a flow final when other branches are still running?

    Show answer

    The activity final terminates the whole activity, including the other branches; a flow final ends only the token that reached it.

  4. Which AWS Step Functions behaviour corresponds to a join with fail-fast semantics?

    Show answer

    The Parallel state: it waits for all branches, and if one fails the state fails and the remaining branches are stopped unless caught.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →