drumlin
drumlin turns a small text format into UML diagrams that look drafted by hand: aligned boxes, even spacing, orthogonal lines and restrained type.
diagram component "Order Platform"
direction right
package "Edge" {
component "Web Gateway" as gateway {
requires IOrders
requires IIdentity
}
}
component "Order Service" as orders {
port api provides IOrders
port events requires IEventBus
component "API Handler" as handler { requires IPricing }
component "Pricing Engine" as pricing { provides IPricing }
component "Outbox" as outbox
api --> handler
handler --> outbox : persist
outbox --> events
}
component "Identity" as identity <<external>> { provides IIdentity }
package "Infrastructure" as infra {
component "Event Bus" as bus { provides IEventBus }
component "Postgres" as db <<database>>
align bus db
}
component "Billing" as billing {
requires IEventBus
provides IInvoices
}
orders ..> db : SQL
note outbox: Publishes at least once. Consumers deduplicate on event id.
Every diagram on this site was rendered from the source shown beside it, while the site was being built. See Rendering at docs build time.
Why drumlin
- It is written in Rust with no runtime. One small binary, with no JVM, browser or Graphviz.
- The syntax is easy to read and write. It follows the feel of PlantUML with one regular grammar.
- Output is deterministic. The same input gives byte-identical SVG, so diagrams diff cleanly in code review.
- Errors are precise. Each has a line, a column and, when possible, a suggestion.
Status
drumlin is a prototype. Component, package, sequence, state and activity diagrams are implemented, and their notation has been audited against UML 2.5.1. Further work and known limitations are on the roadmap.
Finding your way
To draw something, start under Diagrams: each kind has a complete how-to and a page of examples. To see what one symbol looks like, go to Elements.
In the diagram above, matched interface names do the wiring: the portless ends keep socket and lollipop apart, joined by a dashed arrow. Only two simple ports get a joined ball-and-socket. See Interface.
Getting started
Build
drumlin needs a Rust toolchain and has no other dependencies.
cargo build --release
The binary is target/release/drumlin.
Your first diagram
Create hello.uml:
diagram component "Hello"
component Client { requires IGreeting }
component Server { provides IGreeting }
Render it:
drumlin render hello.uml -o hello.svg
Both components name IGreeting, so drumlin wires the socket on the client to the
lollipop on the server with a dashed dependency arrow. You did not have to draw it. Neither
component uses a port; between two ports that have one
interface each, drumlin draws a joined ball-and-socket instead. See
Interface.
Commands
| Command | Effect |
|---|---|
drumlin render <file> | Writes SVG to stdout |
drumlin render <file> -o <out.svg> | Writes SVG to a file |
drumlin check <file> | Validates only. Exit code is 0 when the file is valid |
Grow the diagram
Add a package and an ordinary connector:
diagram component "Hello, again"
package "Backend" {
component Server { provides IGreeting }
component Store <<database>>
Server --> Store : read
}
component Client { requires IGreeting }
Continue with File basics, pick a diagram kind under Diagrams (such as Sequence diagrams), or see all the examples.
File basics
A file may begin with a kind and a title, and may set a direction.
diagram component "Title"
direction right
The kinds implemented are component, package, sequence, state and activity. A file with no header is a
component diagram. The default direction is right for component diagrams and
down for package and state diagrams. See Package diagrams and
State diagrams. direction does not apply to sequence and activity diagrams, which always run top to bottom.
Direction
direction right places dependencies left to right. It suits most component
diagrams, because labels read naturally on horizontal lines.
diagram component "direction right"
direction right
component A
component B
component C
A --> B
B --> C
direction down places dependencies top to bottom. It suits pipelines and layers.
diagram component "direction down"
direction down
component A
component B
component C
A --> B
B --> C
Comments
# and // start a comment that runs to the end of the line.
Working with agents
drumlin suits coding agents, because the text is the design and the picture is for the humans reviewing it.
- One regular grammar. Each statement fits on one line, and nothing depends on visual layout in the source.
- No layout work. An agent states structure. drumlin places every box.
- Deterministic output. Regenerating an unchanged model changes no bytes.
- Errors an agent can act on.
drumlin checkreports a position and a likely fix.
A typical loop
Suggested instruction for an agent
Describe architecture in drumlin (.uml) files. Declare components with
`provides` and `requires`, and let matching interface names wire them. Between
two simple ports they form a ball-and-socket; otherwise socket and lollipop stay
apart, joined by a dashed dependency arrow.
Run `drumlin check` after each edit and fix every reported error. Do not add
layout hints unless a reviewer asks for a specific arrangement.
Component diagrams
A component diagram shows a system’s parts, the interfaces they offer and need, and how
they are wired. It is drumlin’s default: a file with no diagram line is a component
diagram. Write the header anyway when you want a title. The notation follows UML 2.5.1
clauses 11.2, 11.3 and 11.6. The full syntax is in the
grammar reference, and examples
come from real kinds of system.
diagram component "Title"
direction right
The default direction is right, with direction down for layered pictures. See
File basics.
Parts
Declare a component by name, quote names with spaces
and give them a short alias with as. A block nests components, and a <<stereotype>> shows as a
keyword. Group loosely related parts in a package, which is
a folder around them.
diagram component "Parts and grouping"
package "Platform" {
component "Request Router" as router <<service>> {
component Parser
component Dispatcher
Parser --> Dispatcher
}
component "Orders DB" as db <<database>>
router --> db
}
Connecting parts
A connector joins two elements. --> is a solid
connector with an open arrow and ..> is a dashed dependency. Text after : becomes the label.
diagram component "Connectors"
component Producer
component Consumer
component Metrics
Producer --> Consumer : batches
Producer ..> Metrics : reports
Interfaces
Inside a component, provides and requires declare
interfaces. You name them and drumlin draws them. If
an interface has both a requirer and a provider of the same name, drumlin wires them. What it
draws depends on where the two ends are declared, as UML requires:
| Ends | What is drawn |
|---|---|
| Two simple ports (one interface each) | Ball-and-socket: the ball seated in the socket |
| A port with several interfaces at either end | A plain connector named after the interface |
| Anything else (a component with no port at one end) | Socket and lollipop kept apart, joined by a dashed dependency arrow |
An interface with no partner stays a stub on the boundary: a lollipop if it is provided, a socket if it is required.
diagram component "Matched by name, no ports"
component Client { requires IOrders }
component Server { provides IOrders, IHealth }
Ports and delegation
A port is a named point on a component’s boundary, drawn as a small square. A connector from a composite’s port to a part inside it is a delegation: the port forwards to the part. Two ports with one interface each, matched by name, form a ball-and-socket.
diagram component "Ports"
component Gateway {
port uplink requires IEvents
}
component Bus as bus {
port in provides IEvents
component Queue
in --> Queue
}
Notes and layout hints
note target: text attaches a note. Layout hints
such as left-of and align adjust the arrangement and never set coordinates.
diagram component "Notes and hints"
component Cache
component Store
Cache --> Store : writes through
note Cache: Entries expire after 30 seconds.
Next: Examples, or look up what any element looks like under Elements.
Component diagram examples
Component diagrams from several domains, grouped by domain. Each model is illustrative. For the notation, see Component diagrams and the component elements.
Systems software
Monolithic kernel
diagram component "Kernel subsystems"
direction down
component "System Call Interface" as sci
package "Core" as core {
component "Process Scheduler" as sched
component "Memory Manager" as mm
sched --> mm : page faults
}
package "I/O" as io {
component VFS
component "Network Stack" as net
component "Block Layer" as block
VFS --> block
}
component "Device Drivers" as drivers
sci --> sched
sci --> VFS
sci --> net
block --> drivers
net --> drivers
Storage engine
diagram component "LSM storage engine"
direction right
component Client { requires IKeyValue }
component Engine as engine {
port kv provides IKeyValue
component "Write Path" as write
component "Read Path" as read
component Memtable
component "Write-Ahead Log" as wal
component Compactor
kv --> write
kv --> read
write --> wal : append
write --> Memtable : insert
read --> Memtable : probe
}
component "Block Cache" as cache { provides ICache }
component "SSTable Files" as sst <<filesystem>>
engine ..> cache : uses
engine --> sst : flush, read
note wal: Replayed on startup before the engine accepts writes.
Hypervisor
diagram component "Type 1 hypervisor"
direction down
package "Guests" as guests {
component "Linux Guest" as linux { requires IVirtio }
component "RTOS Guest" as rtos { requires IVirtio }
}
component "Virtual Devices" as vdev {
provides IVirtio
requires IDriver
}
package "Hypervisor" as hv {
component "vCPU Scheduler" as vcpu
component "Stage-2 MMU" as mmu
vcpu --> mmu
}
component "Hardware Drivers" as drv { provides IDriver }
guests ..> hv : traps
Container runtime
diagram component "Container runtime"
direction right
component CLI { requires IRuntimeApi }
component Daemon {
provides IRuntimeApi
requires IOci
requires IImages
}
component "Image Store" as images { provides IImages }
component Shim {
provides IOci
requires ISyscalls
}
component "Linux Kernel" as kernel <<platform>> { provides ISyscalls }
note Shim: Outlives the daemon so that containers survive restarts.
Embedded
Motor controller firmware
diagram component "Motor controller firmware"
direction right
package "Application" as app {
component "Speed Loop" as speed
component "Fault Monitor" as fault
fault --> speed : limits
}
package "Drivers" as drivers {
component PWM { provides IPwm }
component ADC { provides IAdc }
component CAN { provides ICan }
}
component "Control Task" as control {
requires IPwm
requires IAdc
}
component Telemetry { requires ICan }
speed --> control : setpoint
note control: Runs at 20 kHz in interrupt context.
Sensor node
diagram component "Battery sensor node"
direction down
component "Sampling Task" as sampler {
requires II2c
provides ISamples
}
component "I2C Driver" as i2c { provides II2c }
component "Radio Task" as radio {
requires ISamples
requires IRadio
}
component "LoRa Driver" as lora { provides IRadio }
component "Power Manager" as power
power ..> sampler : wake
power ..> radio : wake
RTOS layers
diagram component "RTOS layers"
direction down
package "Middleware" as mid {
component "File System" as fs
component "TCP/IP" as tcp
}
package "Kernel" as kernel {
component Tasks
component Queues
component Timers
Tasks --> Queues
Tasks --> Timers
}
component HAL <<port layer>>
fs --> Tasks
tcp --> Tasks
Queues --> HAL
Timers --> HAL
Bootloader
diagram component "Secure boot chain"
direction right
component "ROM" as rom <<immutable>>
component "First Stage" as fsbl
component "Verifier" as verify { provides IVerify }
component "Second Stage" as ssbl { requires IVerify }
component "Kernel Image" as image
rom --> fsbl : verifies
fsbl --> ssbl : loads
ssbl --> image : verifies, starts
note rom: Holds the root public key hash.
Vehicle gateway
diagram component "Vehicle gateway"
direction right
component "Powertrain ECU" as ptrain { port can requires IFrames }
component "Body ECU" as body { port can requires IFrames }
component Gateway {
port bus provides IFrames
port uplink requires IEthernet
component Router
component Firewall
bus --> Firewall
Firewall --> Router
Router --> uplink
}
component "Telematics Unit" as tcu { provides IEthernet }
Compilers and tools
Compiler pipeline
diagram component "Compiler pipeline"
direction down
component Driver { requires ITokens }
package "Front End" as front {
component Lexer { provides ITokens }
component Parser
component "Type Checker" as sema
Lexer --> Parser : tokens
Parser --> sema : AST
}
package "Back End" as back {
component "IR Builder" as ir
component Optimizer
component "Code Generator" as codegen { provides ITarget }
ir --> Optimizer
Optimizer --> codegen : SSA
}
sema --> ir : typed AST
Driver ..> codegen : invoke
Language server
diagram component "Language server"
direction right
component Editor { requires ILsp }
component Server as server {
port lsp provides ILsp
component Dispatcher
component "Incremental Parser" as parser
component "Symbol Index" as index
component Diagnostics
lsp --> Dispatcher
Dispatcher --> parser : didChange
parser --> index : update
parser --> Diagnostics : errors
}
component "File Watcher" as watcher
watcher --> server : changes
Build system
diagram component "Build system"
direction right
component "Build Files" as files <<input>>
component "Graph Builder" as graph
component Scheduler { requires IExecutor, ICache }
component "Local Executor" as local { provides IExecutor }
component "Content Cache" as cache { provides ICache }
files --> graph : parse
graph --> Scheduler : actions
note cache: Keyed by a hash of each action's inputs.
Debugger
diagram component "Debugger"
direction right
component "Debug UI" as ui { requires IDebug }
component Engine {
provides IDebug
requires ISymbols
requires ITarget
}
component "DWARF Reader" as dwarf { provides ISymbols }
component "ptrace Backend" as ptrace { provides ITarget }
This site’s toolchain
diagram component "Documentation build"
direction right
component "Markdown Chapters" as md <<input>>
component mdBook { requires IPreprocessor }
component "mdbook-drumlin" as pre {
provides IPreprocessor
requires IRender
}
component "drumlin library" as lib { provides IRender }
component "Static Site" as site <<output>>
md --> mdBook
mdBook --> site : HTML with inline SVG
Distributed services
Hexagonal service
diagram component "Hexagonal service"
direction right
component "HTTP Adapter" as http { requires IUseCases }
component Core as core {
port api provides IUseCases
port repo requires IRepository
port publish requires IEvents
component "Use Cases" as cases
component "Domain Model" as model
api --> cases
cases --> model
cases --> repo
cases --> publish
}
component "SQL Adapter" as sql { provides IRepository }
component "Kafka Adapter" as kafka { provides IEvents }
Consensus group
diagram component "Replicated state machine"
direction right
component Client { requires IPropose }
component Replica as replica {
port propose provides IPropose
port peers requires IRaftRpc
component "Consensus Module" as raft
component Log
component "State Machine" as sm
propose --> raft
raft --> Log : append
Log --> sm : apply committed
raft --> peers
}
component "Peer Replicas" as others { provides IRaftRpc }
Cache-aside
diagram component "Cache-aside read"
direction right
component Service
component Redis <<cache>>
component Postgres <<database>>
Service --> Redis : 1. get
Service --> Postgres : 2. on miss
Service ..> Redis : 3. set, ttl 60s
API gateway
diagram component "API gateway"
direction right
component "Mobile App" as mobile { requires IPublicApi }
component "Web App" as web { requires IPublicApi }
component Gateway {
provides IPublicApi
requires IAuth
requires ICatalog
requires ICheckout
}
package "Services" as services {
component Auth { provides IAuth }
component Catalog { provides ICatalog }
component Checkout { provides ICheckout }
}
Event-driven checkout
diagram component "Event-driven checkout"
direction right
component Checkout { requires IBus }
component "Message Bus" as bus { provides IBus }
component Payments
component Inventory
component Shipping
bus --> Payments : OrderPlaced
bus --> Inventory : OrderPlaced
Payments --> Shipping : PaymentCaptured
Inventory --> Shipping : StockReserved
note bus: Delivery is at least once. Handlers are idempotent.
Data platforms
ETL pipeline
diagram component "Nightly ETL"
direction right
component "Source Databases" as src <<database>>
component Extract
component Transform
component Load
component Warehouse <<database>>
component "Data Quality" as dq
src --> Extract
Extract --> Transform : parquet
Transform --> Load
Load --> Warehouse
Transform ..> dq : assertions
Stream processor
diagram component "Stream processor"
direction right
component Producers { requires ILog }
component "Partitioned Log" as log {
provides ILog
provides ISubscribe
}
component Job as job {
port source requires ISubscribe
component Windowing
component Aggregator
component Sink
source --> Windowing
Windowing --> Aggregator
Aggregator --> Sink
}
component "State Backend" as state <<rocksdb>>
job ..> state : checkpoints
Query engine
diagram component "Query engine"
direction down
component "SQL Parser" as parser
component Planner { requires ICatalog }
component Catalog { provides ICatalog }
component Optimizer
component Executor
component "Storage Layer" as storage
parser --> Planner : AST
Planner --> Optimizer : logical plan
Optimizer --> Executor : physical plan
Executor --> storage : scans
ML serving
diagram component "Model serving"
direction right
component Client { requires IPredict }
package "Serving" as serving {
component Router { provides IPredict }
component "Model A" as a
component "Model B" as b
Router --> a : 95%
Router --> b : 5%
}
component "Feature Store" as features <<database>>
component "Metrics Sink" as metrics
serving ..> features : lookup
serving ..> metrics : latency
Package diagrams
A package diagram shows how a system is grouped into packages and how those
packages depend on each other. Start a file with diagram package.
diagram package "Title"
direction down
The default direction is down, which suits layered dependencies. direction right
is also available. Both lines are optional except for diagram package, because a
file with no header is a component diagram.
Packages
A package is drawn as a folder. When it shows its contents its name sits in the tab, and when it does not its name sits in the body, as UML 2.5.1 specifies.
diagram package "Names in the tab, or in the body"
package Empty
package Types {
class Integer
class Time
}
Packages nest to any depth, and a <<stereotype>> becomes a keyword above the name.
diagram package "Nesting and keywords"
package "Platform" as pf <<model>> {
package "Kernel" as kernel {
class Scheduler
class Timer
}
package Drivers
Drivers ..> kernel : <<access>>
}
package Empty
Empty left-of pf
Classifiers
class, interface and enum declare simple name boxes. An interface carries the
«interface» keyword and an enumeration the «enumeration» keyword. A declared
stereotype joins that keyword list, except <<abstract>>, which sets the name in italics.
class Order
interface Repository <<abstract>>
enum Status
Relations
| Arrow | Meaning | Drawn as |
|---|---|---|
a ..> b | Dependency | Dashed line, open head |
a --> b | Directed association | Solid line, open head |
a -- b, a .. b | Plain line | Solid or dashed, no head |
a --|> b | Generalization | Solid line, hollow triangle |
a ..|> b | Realization | Dashed line, hollow triangle |
a +-- b | Containment | Solid line, circle-plus on a |
Reversed forms such as <.., <--, <|--, <|.. and --+ swap the two ends.
A <<x>> in a label becomes «x», which is how «import», «access» and «merge»
are written. Any other label text is kept as it is.
diagram package "Relations"
package App
package Lib
package Util
package Base
App ..> Lib : <<import>>
App ..> Util : <<access>>
Lib ..> Base : <<merge>>
Util ..> Base : uses
diagram package "Classifiers"
direction right
interface Shape <<abstract>>
class Circle
class Square
class Base
enum Kind
Circle ..|> Shape
Square ..|> Shape
Circle --|> Base
Base --> Kind : tags
Containment
owner +-- member draws UML’s containment connector, with the circle-plus on the
owner. Use it for members declared outside the owner’s block. Members in the next
rank share one trunk.
diagram package "Containment"
package Types
class Shape
class Point
class Color
Types +-- Shape
Types +-- Point
Types +-- Color
diagram package "Containment, left to right"
direction right
package "api" as api
package "core" as core
class Router
class Handler
core +-- Router
core +-- Handler
api ..> core : <<import>>
A dependency may run between an element and the package drawn around it, for example
a nested package importing its parent. The line runs to the inside of the package
boundary. Containment with +-- needs no line when the member is already drawn inside
the owner, so that is an error.
diagram package "Nested import"
package Core {
package Scheduler
Scheduler ..> Core : <<import>>
}
diagram package "Containment"
package Types { class Shape }
Types +-- Shape
drumlin says:
3:1: error: `Shape` is drawn inside `Types`, so containment needs no line; declare `Shape` outside the `Types` block
Notes and layout hints
Notes and hints work as they do in component diagrams. A note attaches to a package
or a classifier and sits next to it, inside the same container. left-of,
right-of, above, below and align set relative order.
diagram package "Notes and hints"
package Core {
package Parser
package Checker
package Emitter
package Runtime
package Stdlib
Parser left-of Checker
}
class Tool
Tool ..> Core : <<import>>
note Tool: Built separately.
Siblings with no relations between them are folded into a compact block, unless a hint names them.
diagram package "Unconnected members fold into rows"
package Kernel {
class Scheduler
class Timer
class Mutex
class Channel
class Pipe
class Signal
}
Statements on one line
A semicolon separates statements, which suits small packages.
package "Core" as core { class Foo; interface IBar; enum Mode; package inner { } }
Package diagram examples
Layered architecture
A note marks the rule for the domain layer: it depends on nothing else.
diagram package "Layered architecture"
package "Presentation" as ui {
package Web
package Mobile
}
package "Application" as app {
package Commands
package Queries
}
package "Domain" as domain {
class Order
class Customer
interface Repository
enum Status
Order --> Customer
Order ..> Status
}
package "Infrastructure" as infra {
package Persistence
package Messaging
}
ui ..> app : <<import>>
app ..> domain : <<import>>
infra ..> domain : <<access>>
note domain: Depends on nothing else. Keep it free of framework types.
Library dependency graph
Left-to-right flow fits a dependency graph with labelled imports.
diagram package "Library dependency graph"
direction right
package app
package "http-client" as http
package "json" as json
package "tls" as tls
package "logging" as log
package "platform" as plat
app ..> http : <<import>>
app ..> json : <<import>>
app ..> log : <<import>>
http ..> tls : <<access>>
http ..> log
json ..> plat
tls ..> plat
log ..> plat
Package merge
Dashed arrows with the «merge» keyword point from the receiving package to the merged one.
diagram package "Package merge"
package P { class A; class B }
package Q { class C }
package R { class D }
package S
package T
R ..> P : <<merge>>
R ..> Q : <<merge>>
S ..> Q : <<merge>>
T ..> R : <<merge>>
T ..> S : <<merge>>
Plugin system
Realizations cross package boundaries. align keeps the interface in reach of the plugins.
diagram package "Plugin system"
package Host {
class Registry
interface Plugin
Registry --> Plugin : loads
align Registry Plugin
}
package "plugins" as plugins <<framework>> {
package Markdown { class MarkdownPlugin }
package Search { class SearchPlugin }
}
MarkdownPlugin ..|> Plugin
SearchPlugin ..|> Plugin
note plugins: Discovered at startup from the plugin directory.
Domain model
Classifiers relate across packages, and the packages show their dependency too.
diagram package "Domain model"
package Ordering {
class Order
class LineItem
enum OrderState
interface Pricing
Order --> LineItem : contains
Order ..> OrderState
Order ..> Pricing
}
package Catalog {
class Product
class Category
Product --> Category
}
LineItem --> Product
Ordering ..> Catalog : <<import>>
Sequence diagrams
A sequence diagram shows the messages exchanged between participants over time.
Start a file with diagram sequence. The optional title follows it.
diagram sequence "Ordering a pizza"
actor "Customer" as c
participant "Web app" as web
participant "Kitchen" as k
c -> web : place(order) {
web ->> k : cook(order)
return confirmation
}
Participants
participant declares a lifeline drawn as a box, and actor declares one drawn as a
stick figure. A participant has a name, optionally an alias with as, and optionally a
<<stereotype>>. Participants become columns in declaration order, so the file controls
the left-to-right reading order, and a name must be declared before it is used.
diagram sequence "Heads"
actor "Operator" as op
participant "Controller" as ctl <<service>>
participant Disk
op -> ctl : mount()
ctl ->> Disk : spin up
Messages
Arrows select the kind of message, following UML 2.5.1 clause 17.4.4.
| Arrow | Message | Line and head |
|---|---|---|
a -> b | synchronous call | solid, filled head |
a ->> b | asynchronous message | solid, open head |
a --> b | reply | dashed, open head |
A message may carry a label after :. Labels wrap when they are long, and the column
spacing grows to fit the widest label between two lifelines. A label that holds a literal
{ should be quoted: : "set { a }".
A message from a lifeline to itself is drawn as a compact loop with its label on the right. A synchronous self-call also opens a short nested execution bar.
diagram sequence "Kinds of message"
participant Client
participant Server
Client -> Server : request()
Server --> Client : response
Client ->> Server : fire and forget
Server -> Server : log the request
Server ->> Server : schedule cleanup
Execution blocks
Follow a message with { to open an execution on the receiver. The block closes with
}, and execution bars nest and step to the right as calls go deeper. return sends a
reply from inside a block, to the block’s caller. If return is the last statement of
the block, the bar ends there; otherwise it ends where the block ends. Inside a block,
return takes free text, so it needs no quoting. The block may be written on one line.
diagram sequence "Nested calls"
participant "main()" as m
participant Parser
participant Lexer
m -> Parser : parse(source) {
Parser -> Lexer : next() {
Lexer -> Lexer : skip whitespace
return token
}
Parser -> Parser : build tree
return ast
}
Creating and destroying
A dashed arrow to a head drawn at the moment of creation shows object creation: write
sender -> new "Name" as alias : label, with optional <<stereotype>>. Inside a block
the short form create Name : label is also accepted, and it uses the block’s receiver
as the sender. destroy name ends a lifeline with an X. A participant that has been
destroyed cannot be used afterwards; a destroy in only one alt branch stays open for
the other branches.
diagram sequence "Worker lifetime"
participant Pool
Pool -> new "Worker" as w : spawn()
Pool -> w : run(job) {
return result
}
Pool ->> w : stop
destroy w
Found and lost messages
[-> a is a found message: its sender is outside the diagram, drawn as a filled circle
to the left. a ->] is a lost message, leaving towards a filled circle on the right.
Inside a block opened by a found message, return goes back to the outside.
diagram sequence "Boundaries"
participant Bus
participant Handler
[-> Bus : interrupt
Bus -> Handler : dispatch() {
Handler ->] : retransmit (no ack)
return handled
}
Combined fragments
alt, opt, loop, break, critical and neg wrap messages in a frame with the
operator in a pentagon at the top left and a [guard] above the first event. else
adds operands to alt, and adds them to par, and each operand is split by a dashed
line. Frames size themselves around their contents and nest. A loop guard written in
parentheses, such as (1,5), joins the operator as loop(1,5), following the
textual syntax of clause 17.6.4.
diagram sequence "Retries and alternatives"
participant Client
participant Server
participant Cache
loop "(1,3)" {
Client -> Server : fetch(key) {
alt "cached" {
Server -> Cache : get(key)
Cache --> Server : value
} else "miss" {
Server -> Server : recompute
opt "worth caching" {
Server ->> Cache : put(key, value)
}
}
return value
}
}
diagram sequence "Parallel and critical regions"
participant "Task A" as a
participant Lock
participant "Task B" as b
par {
a -> Lock : acquire()
} and {
b -> Lock : acquire()
}
critical "mutex held" {
Lock --> a : granted
a -> a : update state
a -> Lock : release()
}
Interaction references and notes
ref over a, b : Name draws an interaction use: a frame with a ref tab that stands
for another interaction.
note over a: text, note left of a: text and note right of a: text add notes with a
folded corner, on their own row. note over a, b: text spans several lifelines.
diagram sequence "Boot"
participant Bootloader as bl
participant Kernel as k
participant Init as i
ref over bl, k : Verify signature
bl -> k : jump(entry)
note over k: Interrupts stay masked until the scheduler starts.
k -> new "PID 1" as p : exec
note right of p: Everything runs under this process.
Layout
Columns are spaced from the content between them: labels, self-message loops, notes and frame contents. Rows follow an 8px grid, and each row grows to fit its label. Message labels sit above their line, in the gap beside the sender when a message crosses other lifelines, so that text never runs over a lifeline or an execution bar.
Heads stay at the top and are not repeated at the bottom. A diagram has no sd frame:
UML Annex A lets the frame and its heading be omitted when the page bounds the drawing,
and a diagram sequence "Title" title is a caption outside the diagram. A found message
starts, and a lost message ends, on a filled circle (17.4.4), and both keep the arrowhead
of their kind: filled for ->, open for ->>.
The lexer detail worth knowing: a label runs to the end of the line, except for a
comment (# or //) and a space-separated { or }. Quote the label to keep
those as text.
Sequence diagram examples
Six diagrams from systems work. Each uses the notation of the sequence diagram page.
TCP connection: handshake, data and teardown
The three-way handshake, a segment and its acknowledgement, and a graceful close. A lost segment leaves through a lost message.
diagram sequence "TCP connection"
participant "Client" as c
participant "Server" as s
note over c, s: Three-way handshake
c ->> s : SYN seq=x
s ->> c : SYN+ACK seq=y, ack=x+1
c ->> s : ACK ack=y+1
note over c, s: Established
c ->> s : data seq=x+1, len=512
s ->> c : ACK ack=x+513
opt "segment lost" {
c ->] : data seq=x+513
c ->> s : retransmit after RTO
}
note over c, s: Graceful close
c ->> s : FIN
s ->> c : ACK
s ->> c : FIN
c ->> s : ACK
note right of c: TIME_WAIT for 2 MSL before the socket is freed.
Raft log replication
The leader appends locally, replicates to followers in parallel and commits once a majority has acknowledged. One follower rejects the entry.
diagram sequence "Raft: replicating one log entry"
actor "Client" as client
participant "Leader" as leader <<raft>>
participant "Follower 1" as f1
participant "Follower 2" as f2
participant "State machine" as sm
client -> leader : command(x) {
leader -> leader : append to log
par {
leader -> f1 : AppendEntries(term, entry) {
f1 -> f1 : append and persist
return success
}
} and {
leader -> f2 : AppendEntries(term, entry) {
alt "log matches prevLogIndex" {
f2 -> f2 : append and persist
return success
} else "prevLogTerm mismatch" {
return reject (conflict index)
note right of f2: Leader walks nextIndex back and retries.
}
}
}
critical "majority acknowledged" {
leader -> leader : advance commitIndex
leader -> sm : apply(x) {
return result
}
}
return result
}
Interrupt handling with a nested IRQ
A device interrupt enters the CPU as a found message, and a higher priority interrupt preempts the handler. The hardware restores context on return from interrupt.
diagram sequence "Nested interrupt handling"
participant "Timer" as timer
participant "CPU core" as cpu
participant "Kernel entry" as entry
participant "UART ISR" as isr
participant "Timer ISR" as tisr
participant "Scheduler" as sched
[-> cpu : UART IRQ {
cpu ->> entry : vector through IDT
entry -> entry : save registers
entry -> isr : uart_isr() {
isr -> isr : read FIFO
timer ->> cpu : timer IRQ
note over cpu, tisr: Higher priority: the ISR is preempted.
cpu -> tisr : timer_isr() {
tisr ->> sched : mark reschedule
return eoi
}
return eoi
}
entry -> entry : check need_resched
opt "reschedule pending" {
entry -> sched : schedule() {
return next task
}
}
entry ->> cpu : iret
return resumes interrupted task
}
TLS 1.3 handshake
The handshake with a ServerHello carrying the key share, and Finished from both sides. Application data flows once the keys are in place. A failed verification ends with an alert.
diagram sequence "TLS 1.3 handshake"
participant "Client" as c
participant "Server" as s
participant "CA store" as ca
c -> s : ClientHello(key_share, sni, supported_versions) {
s -> s : derive handshake secrets
return ServerHello(key_share)
}
note over c, s: Encrypted from here on: EncryptedExtensions, Certificate, CertificateVerify, Finished
s ->> c : EncryptedExtensions
s ->> c : Certificate
s ->> c : CertificateVerify
c -> ca : verify chain(certificate) {
return ok
}
alt "certificate valid" {
s ->> c : Finished
c ->> s : Finished
note over c, s: Application data, protected by traffic keys
c ->> s : HTTP request
s ->> c : HTTP response
} else "verification failed" {
c ->> s : alert(bad_certificate)
c ->] : close connection
}
RPC with retry and exponential backoff
A client retries a call that times out and gives up after three attempts. A circuit breaker records the outcome.
diagram sequence "RPC with retry"
participant "Client stub" as stub
participant "Circuit breaker" as cb
participant "Service" as svc
loop "(1,3)" {
stub -> cb : allow? {
return true
}
stub -> svc : GetOrder(id, deadline=200ms) {
alt "reply before deadline" {
return order
break "success" {
stub ->> cb : record success
}
} else "deadline exceeded" {
stub -> stub : sleep(backoff with jitter)
stub ->> cb : record failure
}
}
}
stub --> stub : surface error to caller
note right of stub: The stub surfaces the last error once retries run out.
Page fault in a demand-paged kernel
A page fault handled through a lookup and swap-in, showing creation and destruction of a transient object.
diagram sequence "Page fault"
participant "MMU" as mmu
participant "Kernel" as k
participant "Swap device" as swap
[-> mmu : translate(vaddr)
mmu -> k : page fault(vaddr) {
k -> new "Fault context" as fc <<per fault>> : allocate
k -> k : find VMA
alt "anonymous page" {
k -> swap : read(slot) {
note right of swap: The request is queued and the task sleeps.
return page
}
k -> k : map page
} else "invalid access" {
k ->] : SIGSEGV to task
}
destroy fc
return retry the access
}
State diagrams
A state machine diagram starts with diagram state, and may have a title. Notation follows
clause 14 of UML 2.5.1. Flow runs down the page by default, because transition labels sit
beside vertical lines. direction right is available, and places labels above horizontal
lines at the cost of wide rank gaps. The layout reserves room for every label.
diagram state "Door"
[*] --> Closed
Closed --> Opening : open [unlocked] / start motor
Opening --> Open : fully open / stop motor
Open --> Closing : close
Closing --> Closed : fully closed
Closing --> Opening : obstruction / reverse motor
Closed --> Locked : lock
Locked --> Closed : unlock [valid code]
A name used in a transition declares a state in the deepest block that mentions it.
A name that appears only once and is at most two edits from another name is reported
as a typo. To declare such a state on purpose, write state X first.
Use state "Display name" as alias when a state needs a longer name or a body.
Transition labels
A label follows : and has the UML form, in which every part is optional:
trigger {, trigger} [guard] / effect
A long label breaks before the effect, then wraps. Transitions between the same pair of states run side by side with their labels on the outside. A self-transition is a loop on the side of its state, with its label beside it.
diagram state "Transition labels"
direction right
[*] --> Waiting
Waiting --> Armed : arm, rearm [sensors ready] / log("armed")
Armed --> Waiting : disarm
Armed --> Armed : tick / count()
Armed --> Armed : drift [abs(x) > limit] / correct()
Armed --> Firing : trigger [armed and not locked] / ignite(); start_timer()
Firing --> [*] : done
UML restricts some labels, and drumlin reports a violation as an error:
- A transition from a nested initial pseudostate is unlabeled. At the top level it may name the creating event.
- A transition leaving a pseudostate has no trigger. It may have
[guard] / effect. - The segments into a join and out of a fork have no trigger or guard.
Internal behaviours
Inside a state block, entry, do and exit lines go in a compartment under the name. The
expression after / is optional. Any other line with a / and no arrow is an internal
transition, in the same form as a transition label. Each of these takes a whole line.
diagram state "Password entry"
[*] --> Typing
state Typing {
entry / set echo invisible
exit / set echo normal
character / handle character
help / display help
}
Typing --> [*] : enter
Composite states and history
A state block with vertices inside is a composite state, and its contents form its region.
[*] --> X starts the enclosing region at the initial pseudostate, which sits first in the
flow. X --> [*] ends it at the final state. Each region has one of each, scoped to the block
they are written in.
[H] is shallow history and [H*] is deep history. Written alone or in a transition inside a
block, they belong to that block. Name[H] and Name[H*] reach the history of a composite
state from outside it. A transition may also start at, end at or cross the border of a
composite state.
diagram state "Composite states and history"
[*] --> Off
Off --> Playing : power
state Playing {
entry / spin up
do / stream
exit / spin down
[*] --> Track
Track --> Track : next / advance
Track --> Seeking : seek
Seeking --> Track : found
[H]
}
Playing --> Paused : pause
Paused --> Playing[H] : resume
Playing --> Off : power
Paused --> Off : power
Orthogonal regions
region "name" { ... } divides a state into regions, drawn tiled and separated by dashed
lines. The name is optional. Regions sit across the flow: side by side when flowing down,
stacked when flowing right. A transition cannot join two regions of one state.
diagram state "Elevator controller"
[*] --> Idle
Idle --> Operating : call received / plan route
Operating --> Idle : queue empty
Operating --> Fault : motor overcurrent / cut power
Fault --> Idle : technician reset
state Operating {
entry / release brake
exit / engage brake
region "motion" {
[*] --> Moving
Moving --> Stopping : near target floor
Stopping --> Levelled : speed zero
Levelled --> Moving : next call
}
region "doors" {
[*] --> Closed
Closed --> Opening : levelled at floor
Opening --> Open : fully open
Open --> Closing : door timer
Closing --> Closed : fully closed
Closing --> Opening : obstruction
}
}
Pseudostates
choice c a diamond
junction j a small filled circle
fork f a thick bar across the flow; one line in, at least two out
join j a thick bar across the flow; at least two lines in, one out
terminate t a cross
A pseudostate is declared with a name, and is used like a state. Lines fan out from the corners of a choice or junction. A bar widens over the states it connects.
diagram state "Every pseudostate"
[*] --> Ready
Ready --> c : request
choice c
c --> Worker : [workers free] / assign
c --> Queued : [all busy]
Queued --> j : worker free
Worker --> j : done
junction j
j --> f : / prepare
fork f
f --> A
f --> B
state Tasks {
region "left" {
A --> [*]
}
region "right" {
B --> [*]
}
}
Tasks --> T : fatal
terminate T
Entry and exit points
entrypoint name and exitpoint name, written directly in a state block, place a small
circle or a crossed circle on the state’s border, with its name beside it. Transitions from
outside attach on the outer side and transitions from inside on the inner side.
diagram state "Entry and exit points"
[*] --> Idle
Idle --> again : retry
state Login {
entrypoint again
exitpoint aborted
again --> Prompt
Prompt --> Check : submit
Check --> aborted : [locked out]
Check --> [*] : [ok]
}
aborted --> Idle
Login --> Done
Local transitions
A transition between a composite state and a vertex inside it is a local transition, and is drawn to or from the inside of the state’s border, on its right side.
diagram state "Local transitions"
[*] --> Active
state Active {
[*] --> One
One --> Two : next
Two --> Active : reset
}
Active --> [*]
Notes and hints
note <target>: text attaches a note to any state or pseudostate. left-of, right-of,
above, below and align work as in package diagrams, and adjust
layout when the automatic result needs help.
State diagram examples
TCP connection
The TCP connection states from RFC 793, with the segment each transition sends.
diagram state "TCP connection"
[*] --> Closed
Closed --> Listen : passive open / create TCB
Closed --> SynSent : active open / send SYN
Listen --> SynRcvd : rcv SYN / send SYN+ACK
SynSent --> Established : rcv SYN+ACK / send ACK
SynRcvd --> Established : rcv ACK
Established --> FinWait1 : close / send FIN
Established --> CloseWait : rcv FIN / send ACK
FinWait1 --> FinWait2 : rcv ACK
FinWait2 --> TimeWait : rcv FIN / send ACK
CloseWait --> LastAck : close / send FIN
LastAck --> Closed : rcv ACK
TimeWait --> Closed : timeout 2MSL
USB device
The device states of the USB specification. Configured carries its internal behaviours in a compartment.
diagram state "USB device"
[*] --> Attached
Attached --> Powered : hub configured
Powered --> Default : bus reset
Default --> Address : set address
Address --> Configured : set configuration [config != 0]
Configured --> Address : set configuration [config == 0]
Configured --> Suspended : bus idle 3 ms
Suspended --> Configured : bus activity
Powered --> Attached : hub deconfigured
Configured --> Default : bus reset
state Configured {
entry / enable endpoints
exit / disable endpoints
SOF / update frame counter
}
Process lifecycle
A Unix-style process. The self-transition is a compact loop, and a note marks the zombie state.
diagram state "Process lifecycle"
[*] --> New
New --> Ready : admitted
Ready --> Running : dispatch
Running --> Ready : preempt [quantum expired]
Running --> Waiting : wait(event) / block()
Waiting --> Ready : event occurs
Running --> Zombie : exit() / release resources
Zombie --> [*] : reaped by parent
Running --> Running : syscall [fast path] / handle inline
note Zombie: Holds only its exit status until the parent calls wait.
Elevator controller
Motion and doors run in orthogonal regions of one composite state.
diagram state "Elevator controller"
[*] --> Idle
Idle --> Operating : call received / plan route
Operating --> Idle : queue empty
Operating --> Fault : motor overcurrent / cut power
Fault --> Idle : technician reset
state Operating {
entry / release brake
exit / engage brake
region "motion" {
[*] --> Moving
Moving --> Stopping : near target floor
Stopping --> Levelled : speed zero
Levelled --> Moving : next call
}
region "doors" {
[*] --> Closed
Closed --> Opening : levelled at floor
Opening --> Open : fully open
Open --> Closing : door timer
Closing --> Closed : fully closed
Closing --> Opening : obstruction
}
}
OTA firmware update
A download with a nested choice, and a junction that fans out after reboot.
diagram state "OTA firmware update"
[*] --> Idle
Idle --> Downloading : update available / open session
state Downloading {
entry / start watchdog
exit / stop watchdog
chunk received / append to flash
[*] --> Fetching
Fetching --> Verifying : last chunk
Verifying --> c1
choice c1
c1 --> [*] : [hash ok]
c1 --> Fetching : [hash bad] / clear buffer
}
Downloading --> Installing
Downloading --> Idle : network lost
Installing --> Rebooting : image written / mark pending
Rebooting --> j1 : boot complete
junction j1
j1 --> Confirmed : [self test passed]
j1 --> RolledBack : [self test failed] / restore slot A
Confirmed --> [*]
RolledBack --> [*]
Circuit breaker
The resilience pattern, with compact self-transitions on Closed and Open.
diagram state "Circuit breaker"
[*] --> Closed
Closed --> Closed : success / reset failure count
Closed --> Open : failures >= threshold / start timer
Open --> Open : call / fail fast
Open --> HalfOpen : timer expired
HalfOpen --> Closed : probe succeeded / reset failure count
HalfOpen --> Open : probe failed / restart timer
state Closed {
do / count failures
}
Activity diagrams
An activity diagram shows the flow of control and data through a behaviour. Write it with a
diagram activity header, then describe the flow from top to bottom. The notation follows
UML 2.5.1 clause 15 and, for signals, clause 16. Layout is computed from the structure you write,
so nothing needs positioning.
diagram activity "Title"
Actions and flow
start is the initial node, a filled circle. action is a rounded rectangle. Statements in
order are joined by flows with open arrowheads. stop is the activity final node, a bullseye, and end
is the flow final node, a circled X: stop ends the whole activity, end ends only its own
flow. Quote a name that has several words.
diagram activity "Actions in a flow"
start
action "Validate request"
action Handle
object "Order"
action "Store order"
stop
Object nodes
object draws an object node, a plain rectangle, on the flow. Flows into and out of it are
object flows. Since an initial node only starts control flows, object cannot directly follow start.
Decisions
if "guard" { ... } draws a decision and a merge. Guards go on the outgoing flows, in square
brackets, near the decision. else gives the other branch; the guard defaults to the predefined
[else], and else "guard" changes it. Write else if "guard" { ... } for more branches on the
same decision node. An if with no else gets an empty [else] branch, so a decision always has a way on.
diagram activity "Decisions"
start
action "Parse header"
if "valid" {
action "Route request"
} else "malformed" {
action "Log error"
}
if "cached" {
action "Serve from cache"
}
if "rejected" {
action "Send 403"
stop
}
action "Run handler"
if "2xx" {
action "Cache"
} else if "4xx" {
action "Count"
} else if "5xx" {
action "Alert"
} else {
action "Mystery"
}
stop
Two rules keep a straight flow straight. A body beside an empty branch sits on the main axis, and
the empty branch runs around it. When exactly one branch continues and the others end in stop or
end, that branch takes the axis and the others step aside. Otherwise the branches sit side by side
and are centred on the decision.
switch "subject" { case "a" { ... } ... } is a multi-way decision. The subject is shown as a
«decisionInput» note on the diamond, as Figure 15.33 draws it. The subject is optional.
diagram activity "Multi-way decision"
start
switch "HTTP method" {
case "GET" {
action "Read"
}
case "POST" {
action "Create"
}
case "PUT" {
action "Replace"
}
case "else" {
action "405"
}
}
stop
A branch that reaches stop or end does not rejoin the flow, and no merge is drawn for it. If only one
branch rejoins, no merge is needed at all.
Loops
while "guard" { ... } tests before each pass and repeat { ... } until "guard" after it. Both
use a merge for the return and a decision for the test. The back edge runs along the left side,
and the loop’s exit leaves on the right, so neither crosses the body.
diagram activity "Loops"
start
while "queue not empty" {
action "Pop task"
action "Run task"
}
repeat {
action "Read block"
action "Verify CRC"
} until "CRC ok"
stop
A while has its body on the [guard] flow and leaves on [else]. A repeat leaves on [guard] and returns
on [else].
Concurrency
fork { ... } and { ... } draws a fork bar, branches side by side, and a join bar. Every branch needs at
least one step. A branch ending in end or stop does not reach the join; if only one branch reaches
it, the join is left out, and the flow carries on from that branch.
diagram activity "Concurrent flows"
start
fork {
action "Fetch profile"
} and {
action "Fetch orders"
} and {
action "Fetch feed"
}
action "Compose page"
fork {
action "Audit"
end
} and {
action "Respond"
}
stop
Signals
send draws a send signal action, a convex pentagon. accept draws an accept event action, a concave
pentagon. An accept needs no incoming flow, so it may begin an activity instead of start.
diagram activity "Signals"
start
action "Request payment"
send "PaymentRequested"
accept "PaymentConfirmed"
action "Ship goods"
stop
Partitions
lane "Name" { ... } puts steps in a swimlane, drawn as a labelled band. Lanes appear left to right in the order
they are first named, so you can return to a lane by naming it again, and a lane block
inside a branch moves only that branch. Flows between lanes turn at right angles in the free row above their target.
diagram activity "Swimlanes"
start
lane "Customer" {
action "Place order"
}
lane "Warehouse" {
action "Pick items"
if "in stock" {
action "Ship"
} else {
lane "Customer" {
action "Choose substitute"
}
}
}
stop
In a diagram that uses lanes, every action, signal and object must be inside a lane block. Control
nodes may stand outside one, and then belong to the lane the flow is in. A lane inside a lane is not a
sub-partition: it simply moves the steps inside it to that lane.
A decision stays in its own lane and its branches run to their lanes. A fork or join bar spans the lanes of its branches, meaning it belongs to all of them (a node may be in several partitions). Branches that interleave lanes are placed one above the other, so a flow crossing lanes never runs through a node.
Notes
note: text attaches a note to the statement before it, with a dashed line. A note on a simple step sits beside it.
On a decision, while, repeat or fork, it sits above the opening node. A flow’s first statement cannot have
a note, as there is nothing before it.
diagram activity "Notes"
start
action "Load config"
note: Falls back to defaults when the file is missing.
if "debug" {
action "Enable traces"
}
note: Checked once at startup.
action "Run"
stop
Rules from the specification
drumlin rejects flows that UML forbids and stops problems early:
startmust be first in the flow, because an initial node has no incoming edge.- A flow cannot end on a merge, a join or a loop exit, because those need one outgoing edge. Add a step, or
stoporend. - Statements after
stoporendare unreachable, and it is an error to write them.
split from other tools is not offered: use fork for concurrent flows, or switch for alternatives.
Grammar details are in the Reference, and common mistakes in Errors.
Activity diagram examples
Examples for systems developers. Each is a golden test, so what you see is what drumlin produces.
Build and release pipeline
Compile, tests and lint run concurrently. The note attaches to the fork. A release only happens for a tagged
build, so the signing steps sit on the main axis and the empty [else] branch runs around them.
diagram activity "Build and release pipeline"
start
action "Fetch sources"
action "Resolve dependencies"
object "Lockfile"
fork {
action "Compile"
action "Link"
} and {
action "Run unit tests"
} and {
action "Lint and audit"
}
note: Compile, test and lint run concurrently.
action "Package artifacts"
if "tag build" {
action "Sign artifacts"
send "Publish release"
}
action "Notify the team"
stop
Interrupt service with deferred work
The top half is the hard interrupt handler; the bottom half is the deferred work. The handler is started by an accept event rather than an initial node, and hands off with a signal.
diagram activity "Interrupt service with deferred work"
accept "IRQ line raised"
action "Acknowledge at the controller"
action "Read status register"
if "FIFO has data" {
action "Copy bytes to the ring buffer"
send "Wake softirq"
} else "overrun" {
action "Count overrun"
}
action "Return from interrupt"
accept "softirq scheduled"
while "ring buffer not empty" {
action "Pop one packet"
if "checksum valid" {
action "Deliver to the stack"
} else {
action "Drop and count"
}
}
end
Connection retry
A repeat wraps the handshake with backoff, and early failures step aside so that the successful
path stays straight.
diagram activity "Connection retry with backoff"
start
action "Resolve host"
if "resolved" {
repeat {
action "Open socket"
action "TLS handshake"
if "failed" {
action "Wait for backoff"
}
} until "connected or attempts exhausted"
if "connected" {
action "Send greeting"
} else {
action "Report unreachable"
stop
}
} else {
action "Report DNS failure"
stop
}
action "Mark session ready"
stop
Deployment with parallel checks across lanes
The fork spans the lanes running the checks, and the release decision returns to the developer’s lane.
diagram activity "Deployment with parallel checks"
start
lane "Developer" {
action "Merge to main"
}
lane "CI" {
action "Build image"
send "Image ready"
}
lane "Staging" {
accept "Image ready"
action "Deploy to staging"
}
fork {
lane "Staging" {
action "Smoke tests"
}
} and {
lane "Security" {
action "Scan image"
}
} and {
lane "CI" {
action "Contract tests"
}
}
lane "Developer" {
if "all green" {
lane "Production" {
action "Roll out canary"
}
} else {
action "Roll back and file issue"
}
}
stop
Firmware boot
A switch on the boot slot, with «decisionInput» naming what the guards test.
diagram activity "Firmware boot"
start
action "Clock and RAM init"
if "reset cause is brown-out" {
action "Record fault"
}
switch "boot slot" {
case "A" {
action "Verify slot A"
}
case "B" {
action "Verify slot B"
}
case "else" {
action "Enter recovery"
end
}
}
if "signature valid" {
action "Jump to application"
note: The vector table is relocated first.
} else {
action "Enter recovery"
}
stop
Elements of component diagrams
| Element | Syntax |
|---|---|
| Component | component "Name" as a <<stereotype>> { ... } |
| Interface | provides I, requires I |
| Port | port p left provides I |
| Connector | a --> b : label |
| Package | package "Name" { ... } around components |
| Note | note a: text |
diagram component "All the component elements"
component Client { requires IApi }
package "Backend" {
component Service {
port api provides IApi
component Worker
api --> Worker
}
component Database <<database>>
Service ..> Database : queries
}
note Client: Retries on timeout.
See Component diagrams for how to write one.
Component
A component is a rectangle with the component icon at its top right and its name inside. The simplest needs only a name.
component Scheduler
Names and aliases
A name with spaces needs quotes. Use as to give it a short alias for references.
component "Memory Allocator" as alloc
component "Page Table" as pages
alloc --> pages
Stereotypes
A stereotype goes in << and >>.
component "Orders DB" as db <<database>>
component "Billing API" as api <<external>>
api --> db
Nesting
Components can contain components. The outer one becomes a composite.
component "Network Stack" as net {
component Socket
component TCP
component IP
Socket --> TCP
TCP --> IP
}
Nesting can go several levels deep.
component Kernel {
component "I/O" as io {
component VFS
component Block
VFS --> Block
}
component Memory
io --> Memory
}
One-line blocks
A short body can share the line with its braces.
component Sensor { provides ISample }
component Logger { requires ISample }
Syntax
component <name> [as <alias>] [<<stereotype>>] [{ ... }]
Specification
UML 2.5.1, 11.6.4 (p. 210). A component is a classifier rectangle; showing the icon lets the «component» keyword be hidden, which drumlin does. In a composite the name is centred, with any keyword above it (9.2.4, p. 101).
Used in
Monolithic kernel, Storage engine, Compiler pipeline.
Interface
A provided interface is a lollipop, a small circle on a stem. A required interface is a socket, a
half-circle on a stem. Interfaces are declared inside a component with provides and requires,
and drumlin decides how to draw them.
Assembly
A required and a provided interface with the same name are wired together. Which notation drumlin uses depends on where the interfaces are declared, following UML 2.5.1, clause 11.2.4.
Between two simple ports (ports with one interface each), drumlin draws a ball-and-socket assembly connector with the ball inside the socket. The notation is reserved for simple ports, so anything else keeps the socket and the lollipop apart: when an end has no port, the socket and lollipop are joined by a dashed dependency arrow from socket to lollipop, as in Figure 11.46. A port with more than one interface is joined with a plain connector named after the interface.
With no ports:
component Consumer { requires IQueue }
component Broker { provides IQueue }
Between two simple ports:
component Consumer { port in requires IQueue }
component Broker { port out provides IQueue }
Unconnected interfaces
An interface with no partner is drawn as a stub at the component boundary. A provided interface has a lollipop and a required interface has a socket.
component Codec {
provides IEncode
provides IDecode
requires IAllocator
}
Several interfaces
Separate names with commas.
component Runtime { provides ITimer, IThreads, IFiles }
component App { requires ITimer, IThreads, IFiles }
Several consumers
Each consumer gets its own connection to the provider.
component Logger { provides ILog }
component Net { requires ILog }
component Disk { requires ILog }
component UI { requires ILog }
Inside composites
Matching also works inside a composite and across nesting levels.
component App {
component Controller { requires IRepo }
component Repo { provides IRepo }
}
Syntax
provides <interface> [, <interface>]...
requires <interface> [, <interface>]...
Both go inside a component. On a port, use port p provides I or port p requires I. See
Port. In a package diagram an interface is a boxed classifier instead:
see Classifier.
Specification
UML 2.5.1, 10.4.4 (p. 172): lollipop and socket; a dependency arrow from a usage to a realization may join socket to lollipop. Components: 11.6.4 (p. 210) and Figure 11.46 (p. 214). Ball-and-socket: 11.2.4 (p. 186), which permits it only for simple ports and forbids it for complex ports and parts without ports.
Used in
Compiler pipeline, Debugger, API gateway, Container runtime.
Port
A port is a small square on a component’s boundary, with its name beside it. It is a named interaction point.
component Modem {
port serial
port radio
}
Ports with interfaces
A port usually provides or requires one interface, which makes it a simple port. Two simple ports matched by name form a ball-and-socket. A port listing more than one interface is complex: its connector is drawn plain and named after the interface. See Interface.
component Radio { port rx provides IPackets }
component Stack { port input requires IPackets }
An unconnected port interface draws as a stub on the port.
component Controller {
port ctl provides IControl
port bus requires ICan
}
Choosing a side
drumlin chooses a side from a port’s connections. To override it, write left,
right, top or bottom.
component Board {
port power top
port ground bottom
port uart left
port spi right
}
Delegation
A connector from a composite’s port to an inner part delegates the interaction, and is drawn solid.
Refer to a port by its bare name when it is unique, or as component.port.
component Client { requires IApi }
component Service as svc {
port api provides IApi
port store requires IStore
component Router
component Worker
api --> Router
Router --> Worker
Worker --> store
}
component Database { provides IStore }
Syntax
port <name> [left | right | top | bottom] [provides <i> | requires <i>]...
Specification
UML 2.5.1, 11.3.4 (p. 191): a small square with the name near it, placed on or inside the boundary. Delegation connector: 11.3.3 (p. 191) and Figure 11.47 (p. 214). Port type labels and multiple interfaces on one lollipop are optional UML notation that drumlin does not draw.
Used in
Hexagonal service, Vehicle gateway, Storage engine.
Connector
A connector is a line between two elements. drumlin draws a solid line for a connector and a dashed line for a dependency; both may end in an open arrow.
| Syntax | Meaning |
|---|---|
a --> b | Solid line, open arrow at b |
a ..> b | Dashed dependency, open arrow at b |
a -- b | Solid line, no arrow |
a .. b | Dashed line, no arrow |
a <-- b | Same as b --> a |
a <.. b | Same as b ..> a |
component A
component B
component C
component D
A --> B
A ..> C
B -- D
C .. D
Labels
Text after : labels a connector.
component Producer
component Consumer
Producer --> Consumer : batches, 64 KiB max
Direction and rank
A connector places its source before its target, along the diagram direction. Cycles are allowed, and drumlin reverses one edge internally to rank them.
component Parser
component Lexer
component Preprocessor
Parser --> Lexer : next token
Lexer --> Preprocessor : include
Preprocessor --> Parser : macro expansion
Connectors drawn from interface names
Interface matching makes connectors too, without any arrow in the source. Two simple ports form a ball-and-socket assembly connector, a port with several interfaces gets a plain connector named after the interface, and an end with no port keeps its socket and lollipop apart with a dashed arrow. See Interface. A connector from a composite’s port to a part inside is a delegation.
Syntax
<ref> (--> | ..> | -- | .. | <-- | <..) <ref> [: <label>]
A connector needs two different components. A <<x>> in a label is written with guillemets, «x».
Specification
UML 2.5.1. Connectors: 11.2.4 (p. 186). Dependency: 7.7.4 (p. 39). An arrow on a connector end is an end adornment, as for an association (11.2.4, p. 186).
Used in
Cache-aside, Event-driven checkout, Build system.
Elements of package diagrams
| Element | Syntax |
|---|---|
| Package | package "Name" { ... } |
| Classifier | class, interface, enum |
| Relations | a ..> b, a --> b, a --|> b, a ..|> b |
| Import and merge | a ..> b : <<import>> |
| Containment | a +-- b |
| Note | note a: text |
diagram package "All the package elements"
direction right
package App {
class Handler
}
package Core {
interface Store
class SqlStore
SqlStore ..|> Store
}
App ..> Core : <<import>>
note App: Only depends on Core.
See Package diagrams for how to write one.
Package
A package is a folder: a rectangle with a small tab. When it shows its contents its name sits in the tab, and when it shows none its name sits in the body. In component diagrams it groups components; in package diagrams it is the main symbol.
Inside a package diagram:
diagram package "Tab or body"
package Empty
package Types { class Point }
In a component diagram:
package "Domain" {
component Orders
component Customers
Orders --> Customers
}
Connectors may cross package boundaries.
package "Application" as app {
component Handlers
}
package "Domain" as domain {
component Model
}
package "Adapters" as adapters {
component SQL
component Kafka
align SQL Kafka
}
Handlers --> Model
SQL ..> Model : implements
Kafka ..> Model : implements
Packages can nest.
package "Platform" {
package "Storage" {
component Blob
component Index
Index --> Blob
}
package "Compute" {
component Runner
}
Runner --> Index
}
A package may also sit inside a component, as one of its packaged elements.
Syntax
package <name> [as <alias>] [<<stereotype>>] [{ ... }]
Specification
UML 2.5.1, 12.2.4 (p. 248). Packaged elements in a component: 11.6.3 (p. 210).
Used in
Layered architecture, Monolithic kernel, RTOS layers.
Classifier
class, interface and enum declare a named box. An interface carries the keyword
«interface», and an enumeration «enumeration». Other stereotypes join the keywords; <<abstract>>
sets the name in italics instead.
diagram package "Classifiers"
direction right
class Order
interface Repository
enum Status
class Shape <<abstract>>
Looking for lollipops and sockets? Those are the interfaces of component diagrams.
Syntax
class|interface|enum <name> [as <alias>] [<<stereotype>>]
Classifiers cannot have bodies, so attributes and operations are not listed.
Specification
UML 2.5.1. Classifier notation and italics for abstract classifiers: 9.2.4 (p. 101). «interface» keyword: 10.4.4 (p. 172). «enumeration» keyword: 10.2.4 (p. 168).
Used in
Relations
Relations between packages and classifiers in a package diagram.
| Arrow | Relation | Drawn as |
|---|---|---|
a ..> b | Dependency | Dashed line, open head |
a --> b | Directed association | Solid line, open head |
a -- b | Association | Solid line, no head |
a .. b | Plain dashed line | Dashed line, no head |
a --|> b | Generalization | Solid line, hollow triangle |
a ..|> b | Realization | Dashed line, hollow triangle |
diagram package "Relations"
direction right
interface Printable
class Report
class Invoice
class Ledger
Invoice --|> Report
Report ..|> Printable
Report --> Ledger : writes
Ledger ..> Printable : uses
Syntax
a ..> b [: label] a --> b a -- b a .. b a --|> b a ..|> b
Reversed forms such as <.., <--, <|-- and <|.. swap the two ends. A <<x>> in a label becomes «x».
Specification
UML 2.5.1. Dependency and Realization: 7.7.4 (p. 39). Association, open arrowhead for navigable end: 11.5.4 (p. 202). Generalization: 9.2.4 (p. 102).
Used in
Import and merge
A dashed arrow with an open head and a keyword in guillemets. «import» makes another package’s names
public here, «access» makes them private, and «merge» merges the target into the source. The arrow
points from the importing or receiving package to the imported or merged one.
diagram package "Import, access, merge"
direction right
package App
package Lib
package Util
package Base
App ..> Lib : <<import>>
App ..> Util : <<access>>
Lib ..> Base : <<merge>>
A nested package may import its parent, with the line ending on the inside of the boundary:
diagram package "Nested import"
package Core {
package Scheduler
Scheduler ..> Core : <<import>>
}
Syntax
a ..> b : <<import>>
a ..> b : <<access>>
a ..> b : <<merge>>
Any <<x>> in a relation label becomes «x», so other keywords work the same way.
Specification
UML 2.5.1. Import and access: 7.4.4.3 (p. 29). Merge: 12.2.4 (p. 248).
Used in
Package merge, Library dependency graph, Layered architecture.
Containment
Membership is usually shown by nesting. When it is clearer to draw a line, owner +-- member puts a
circle-plus on the owner. Members in the next rank share one trunk.
diagram package "Containment"
package Types
class Shape
class Point
Types +-- Shape
Types +-- Point
Syntax
owner +-- member
member --+ owner
Use it for members declared outside the owner’s block. A member drawn inside its owner needs no line, so
+-- to it is an error.
Specification
UML 2.5.1, 12.2.4 (p. 248).
Used in
Package diagrams: containment.
Elements of sequence diagrams
| Element | Syntax |
|---|---|
| Lifeline | participant, actor |
| Message | a -> b, a ->> b, a --> b, [-> a, a ->] |
| Execution | a -> b : call { ... }, return |
| Creation and destruction | a -> new "B" as b, create, destroy |
| Combined fragment | alt, opt, loop, break, critical, neg, par |
| Interaction use | ref over a, b : Name |
| Note | note over a: text |
diagram sequence "All the sequence elements"
actor User as u
participant Server as s
u -> s : request() {
opt "cached" {
return hit
}
s -> new "Worker" as w : start
destroy w
return result
}
note over s: Stateless.
See Sequence diagrams for how to write one.
Lifeline
A lifeline is a box or a stick figure at the top, with a vertical dashed line below it. Declaration order sets the left-to-right order, and a name must be declared before use.
diagram sequence "Heads"
actor Operator as op
participant "Controller" as ctl <<service>>
op -> ctl : mount()
Syntax
participant <name> [as <alias>] [<<stereotype>>]
actor <name> [as <alias>] [<<stereotype>>]
Heads stay at the top and are not repeated at the bottom.
Specification
UML 2.5.1, 17.3.4.1 (p. 572). The head is a rectangle, or the classifier’s shape; a stick figure for an actor.
Used in
Raft log replication, TCP connection.
Message
A message is a horizontal arrow between lifelines, its kind shown by the line and head.
| Arrow | Message | Line and head |
|---|---|---|
a -> b | synchronous call | solid, filled head |
a ->> b | asynchronous | solid, open head |
a --> b | reply | dashed, open head |
[-> a | found (from outside the diagram) | filled circle at the start |
a ->] | lost (leaves the diagram) | filled circle at the end |
diagram sequence "Message kinds"
participant Client
participant Server
[-> Client : wake
Client -> Server : call()
Server --> Client : reply
Client ->> Server : notify
Server ->] : lost
A self-message draws a compact loop on its lifeline.
Syntax
<a> (-> | ->> | -->) <b> [: label] [{ ... }]
[-> <a> [: label] [{ ... }]
<a> ->] [: label]
A label runs to the end of the line, apart from a comment or a space-separated { or }; quote it to
keep those as text.
Specification
UML 2.5.1, 17.4.4.1 (p. 576): filled heads for synchronous, open for asynchronous and dashed lines for reply. Found and lost messages end on a small black circle (17.4.4, p. 577).
Used in
TCP connection, TLS 1.3 handshake.
Execution
An execution is a thin white bar on a lifeline, showing where it is active. Write a block after a message to open one. Bars nest and step to the right as calls go deeper.
diagram sequence "Nested executions"
participant App
participant Parser
App -> Parser : parse() {
Parser -> Parser : next token
return ast
}
return sends a reply from inside a block, to the block’s caller, and ends the bar when it is
the last statement.
Syntax
<a> -> <b> : label { ... }
return [text]
A plain synchronous self-call also opens a short nested bar.
Specification
UML 2.5.1, 17.2.4.4 (p. 569).
Used in
Interrupt handling with a nested IRQ, Raft log replication.
Creation and destruction
A created lifeline’s head is drawn at the moment of creation, and the creating message is dashed with an
open head. destroy ends a lifeline with an X.
diagram sequence "Worker lifetime"
participant Pool
Pool -> new "Worker" as w : spawn()
Pool ->> w : stop
destroy w
Syntax
<a> -> new <name> [as <alias>] [<<stereotype>>] [: label] [{ ... }]
create <name> [as <alias>] [<<stereotype>>] [: label] [{ ... }] inside a block
destroy <a>
A destroyed lifeline cannot be used afterwards. A destroy in one alt branch leaves the lifeline alive in the
others.
Specification
UML 2.5.1. Creation messages: 17.4.4 (p. 577). DestructionOccurrenceSpecification: 17.4.4.2 (p. 578).
Used in
Page fault in a demand-paged kernel.
Combined fragment
A frame around messages with the operator in a pentagon at its top left. Each operand has a [guard] above
its first event, and dashed lines divide operands.
diagram sequence "Alternatives"
participant Client
participant Server
alt "ok" {
Client -> Server : get
} else "retry" {
loop "(1,3)" {
Client ->> Server : retry
}
}
Operators: alt with else, opt, loop, break, critical, neg, and par with and.
loop "(1,3)" becomes loop(1,3), which is the spec’s textual form.
Syntax
alt ["guard"] { ... } [else ["guard"] { ... }]...
par ["guard"] { ... } [and ["guard"] { ... }]...
opt|loop|break|critical|neg ["guard"] { ... }
Specification
UML 2.5.1. Operands, constraints and frames: 17.6.4.1, 17.6.4.2, 17.6.4.3 (p. 585). Loop:
17.6.4.9 (p. 586). Coregions are an optional shorthand; par covers them.
Used in
Raft log replication, RPC with retry and exponential backoff, TLS 1.3 handshake.
Interaction use
A frame with a ref tab that stands for another interaction, spanning the lifelines it involves.
diagram sequence "Boot"
participant Bootloader as bl
participant Kernel as k
ref over bl, k : Verify signature
bl -> k : jump(entry)
Syntax
ref over <a>[, <b>]... : <name>
Specification
UML 2.5.1, 17.7.4.1 (p. 591).
Used in
Sequence diagrams: references and notes.
Elements of state diagrams
| Element | Syntax |
|---|---|
| State | state Name { ... }, entry /, do /, exit / |
| Region | region "name" { ... } |
| Pseudostates | [*], choice, junction, fork, join, terminate, [H], [H*] |
| Entry and exit points | entrypoint e, exitpoint x |
| Transition | a --> b : trigger [guard] / effect |
| Note | note a: text |
diagram state "All the state elements"
[*] --> Idle
Idle --> Busy : start [ready] / log()
state Busy {
entry / open()
[*] --> Work
Work --> [*]
}
Busy --> [*]
See State diagrams for how to write one.
State
A state is a rectangle with rounded corners. Internal behaviours go in a compartment under the name. A composite state holds other states.
diagram state "Simple and composite"
[*] --> Idle
Idle --> Running : start
state Running {
entry / open()
do / work()
[*] --> Step
Step --> [*]
}
Running --> [*]
Syntax
state <name> [as <alias>] [<<stereotype>>] [{ ... }]
entry|do|exit [/ expression] inside a state, one per line
trigger {, trigger} [guard] / effect internal transition, one per line
A name used in a transition declares a state. See State diagrams.
Specification
UML 2.5.1, 14.2.4.4 (p. 319). The final state is 14.2.4.5 (p. 327).
Used in
USB device, Process lifecycle.
Region
A region is one orthogonal part of a composite state, drawn as a tile with dashed separators. Regions lie across the flow: side by side when flowing down, stacked when flowing right.
diagram state "Regions"
state Active {
region "input" {
[*] --> Reading
Reading --> [*]
}
region "output" {
[*] --> Writing
Writing --> [*]
}
}
Syntax
region ["name"] { ... } inside a state
A transition cannot join two regions of one state.
Specification
UML 2.5.1, 14.2.4.3 (p. 319).
Used in
Pseudostates
| Pseudostate | Syntax | Symbol |
|---|---|---|
| Initial | [*] --> x | Filled circle |
| Final state | x --> [*] | Bull’s-eye |
| Choice | choice c | Diamond |
| Junction | junction j | Small filled circle |
| Fork | fork f | Solid bar, one in and at least two out |
| Join | join j | Solid bar, at least two in and one out |
| Terminate | terminate t | Cross |
| Shallow history | [H], Name[H] | Circle with H |
| Deep history | [H*], Name[H*] | Circle with H* |
diagram state "Choice, fork, join"
[*] --> Ready
Ready --> c : request
choice c
c --> f : [ok]
c --> [*] : [denied]
fork f
f --> A
f --> B
A --> j
B --> j
join j
j --> Done
Done --> [*]
The final state is not strictly a pseudostate but is listed here because it is drawn with them.
Syntax
choice|junction|fork|join|terminate <name>
[*] --> a a --> [*] [H] [H*] Name[H] Name[H*]
Specification
UML 2.5.1, 14.2.4.6 (p. 327). FinalState: 14.2.4.5 (p. 327). Multiplicities of fork, join, choice and junction, and the forbidden triggers on pseudostate transitions, are constraints of clause 14.5, which drumlin checks and reports as errors.
Used in
OTA firmware update, State diagrams: pseudostates.
Entry and exit points
A small circle on a state’s border for an entry point, and a crossed circle for an exit point, each with its name beside it. Outside transitions attach on the outer side, inside ones on the inner side.
diagram state "Entry and exit points"
[*] --> Idle
Idle --> again : retry
state Login {
entrypoint again
exitpoint aborted
again --> Prompt
Prompt --> aborted : [locked out]
}
aborted --> Idle
Syntax
entrypoint <name> inside a state
exitpoint <name> inside a state
Specification
UML 2.5.1, 14.2.4.6 (p. 328).
Used in
State diagrams: entry and exit points.
Transition
A solid line with an open head, labeled trigger [guard] / effect. Every part is optional. A self-transition is
a compact loop. A transition between a composite state and a vertex inside it is local, drawn to or from the
inside of the border.
diagram state "Transitions"
direction right
[*] --> Waiting
Waiting --> Armed : arm [ready] / log()
Armed --> Armed : tick / count()
Armed --> [*] : done
Syntax
a --> b [: trigger {, trigger} [guard] / effect]
b <-- a
A long label breaks before the effect, then wraps. Pseudostate transitions have limits on which parts they may carry: see Pseudostates and the transition labels section.
Specification
UML 2.5.1. Notation and label syntax: 14.2.4.8 (p. 331). Local and external transitions: 14.2.4.9 (p. 334).
Used in
TCP connection, Circuit breaker.
Elements of activity diagrams
| Element | Syntax |
|---|---|
| Action | action "Name" |
| Initial and final nodes | start, stop, end |
| Decision and merge | if, switch, while, repeat |
| Fork and join | fork { ... } and { ... } |
| Object node | object "Name" |
| Signals | send "Name", accept "Name" |
| Partition | lane "Name" { ... } |
| Note | note: text |
diagram activity "All the activity elements"
start
action "Receive"
if "valid" {
action "Process"
} else {
action "Reject"
}
stop
See Activity diagrams for how to write one.
Action
A rectangle with rounded corners. Actions in order are joined by flows with open arrowheads.
diagram activity "Actions"
start
action "Validate request"
action Handle
stop
Syntax
action "name"
A name that is a single word needs no quotes.
Specification
UML 2.5.1. Executable nodes: 15.5.4.1, Figure 15.61 (p. 404). Actions: 16.2.4 (p. 446). Flows: 15.2.4 (p. 379).
Used in
Build and release pipeline, Connection retry.
Initial and final nodes
start is the initial node, a filled circle. stop is the activity final, a bull’s-eye, ending the whole activity.
end is the flow final, a circled X, ending only its own flow.
diagram activity "Control nodes"
start
fork {
action "Audit"
end
} and {
action "Respond"
}
stop
Syntax
start
stop
end
start must be first in the flow. Statements after stop or end are unreachable and an error.
Specification
UML 2.5.1, 15.3.4 (p. 391).
Used in
Firmware boot, Interrupt service with deferred work.
Decision and merge
A diamond. A decision has guards in square brackets on its outgoing flows; a merge joins flows again. if draws
both. while and repeat build a loop from a merge and a decision.
diagram activity "Decision and loop"
start
if "valid" {
action "Accept"
} else "malformed" {
action "Reject"
}
while "more input" {
action "Read"
}
stop
An omitted guard after else is the predefined [else]. switch "subject" shows the subject as a «decisionInput»
note on the diamond.
Syntax
if "guard" { ... } [else if "guard" { ... }]... [else ["guard"] { ... }]
switch ["subject"] { case "guard" { ... } ... }
while "guard" { ... }
repeat { ... } until "guard"
Specification
UML 2.5.1. Decision and merge: 15.3.4 (p. 391). «decisionInput»: Figure 15.33 (p. 390). Guards and [else]:
15.2.4 (p. 380).
Used in
Firmware boot, Connection retry.
Fork and join
A solid bar. A fork splits one flow into concurrent flows, and a join waits for them all.
diagram activity "Fork and join"
start
fork {
action "Fetch profile"
} and {
action "Fetch orders"
}
action "Compose page"
stop
A branch ending in end or stop does not reach the join. When only one branch reaches it, the join is
left out. In lanes, a bar spans the lanes of its branches.
Syntax
fork { ... } and { ... } [and { ... }]...
Each branch needs at least one step.
Specification
UML 2.5.1, 15.3.4 (p. 391).
Used in
Build and release pipeline, Deployment with parallel checks.
Object node
A plain rectangle on the flow. Flows into and out of it are object flows.
diagram activity "Object on the flow"
start
action "Resolve dependencies"
object "Lockfile"
action "Compile"
stop
Syntax
object "name"
An object cannot directly follow start, as an initial node starts only control flows.
Specification
UML 2.5.1, 15.4.4 (p. 399).
Used in
Signals
send draws a send signal action, a convex pentagon. accept draws an accept event action, a concave pentagon. An
accept needs no incoming flow, so it may begin an activity instead of start.
diagram activity "Signals"
start
action "Request payment"
send "PaymentRequested"
accept "PaymentConfirmed"
action "Ship goods"
stop
Syntax
send "name"
accept "name"
Specification
UML 2.5.1. Send signal: 16.3.4.3 (p. 455). Accept event: 16.10.4 (p. 474).
Used in
Interrupt service with deferred work, Deployment with parallel checks.
Partition
A swimlane band with its name at the top. Lanes appear in the order they are first named.
diagram activity "Swimlanes"
start
lane "Customer" {
action "Place order"
}
lane "Warehouse" {
action "Pick items"
}
stop
A lane inside a lane is not a sub-partition: it simply moves the steps inside it to that lane. In a diagram with lanes,
actions, signals and objects must be inside a lane.
Syntax
lane "name" { ... }
Specification
UML 2.5.1, 15.6.4 (p. 408). A node may belong to several partitions, which is how a fork bar spans lanes.
Used in
Deployment with parallel checks.
Note
A note is a rectangle with its upper right corner folded, joined to its target by a dashed line. It attaches free text to an element, and drumlin wraps the text.
component Cache
note Cache: Write-through. Entries expire after 30 seconds.
A note sits next to its target, inside the same container.
package "Storage" {
component Journal
component Pages
Journal --> Pages
note Journal: fsync before every commit record.
}
Attach several notes where they help.
component Reader
component Writer
Reader --> Writer : wakes
note Reader: Lock-free. May retry.
note Writer: Single owner only.
Other kinds
Notes work in every diagram kind, with the syntax of that kind:
diagram state "A note on a state"
[*] --> Idle
Idle --> [*]
note Idle: Waits here between jobs.
In a sequence diagram, write note over a: text, note left of a: text or note right of a: text.
There the line is suppressed, as the spec allows when the context is clear. In an activity diagram,
write note: text after a statement to attach it.
Syntax
note <target>: text component, package and state diagrams
note over <a>[, <b>]... : text sequence diagrams
note left of <a>: text
note right of <a>: text
note: text activity diagrams, on the statement before it
Specification
UML 2.5.1, 7.2.4 (p. 22): a Comment is a rectangle with the upper right corner bent, connected by a dashed line, which may be suppressed if the context is clear.
Used in
Storage engine, Layered architecture, Process lifecycle, TCP connection, Build and release pipeline.
Grammar by kind
Each statement occupies one line. A { ... } body may share the line.
Component diagrams
diagram component ["Title"]
direction (right | down)
package <name> [as <alias>] [<<stereotype>>] [{ ... }]
component <name> [as <alias>] [<<stereotype>>] [{ ... }]
provides <interface> [, <interface>]...
requires <interface> [, <interface>]...
port <name> [left | right | top | bottom] [provides <i> | requires <i>]...
<ref> (--> | ..> | -- | .. | <-- | <..) <ref> [: <label>]
note <element>: <text>
align <element> <element>...
<element> (left-of | right-of | above | below) <element>
A <name> is a bare word or a "quoted string". Bare words may contain letters,
digits, _, and single - or . between them.
An element is referenced by its alias, or by its name when it has no alias.
A <ref> is an element, a unique port name, or <element>.<port>.
Rules
provides,requiresandportbelong inside a component.- A package may be nested inside a component (it is one of its packaged elements).
- A connector needs two different components.
- A requirer and a provider of the same interface name are wired automatically. See Interface.
- Declarations may come after the connectors that use them.
Package diagrams
diagram package ["Title"]
direction right|down
package <name> [as <alias>] [<<stereotype>>] [{ ... }]
class|interface|enum <name> [as <alias>] [<<stereotype>>]
a ..> b [: label] a --> b a -- b a .. b a --|> b a ..|> b a +-- b
note <target>: text
a left-of|right-of|above|below b
align a b c
Statements can be separated with ; as well as newlines. See
Package diagrams.
Sequence diagrams
diagram sequence ["Title"]
actor|participant <name> [as <alias>] [<<stereotype>>]
<a> (-> | ->> | -->) <b> [: label] [{ ... }]
<a> -> new <name> [as <alias>] [<<stereotype>>] [: label] [{ ... }]
create <name> [as <alias>] [<<stereotype>>] [: label] [{ ... }] inside a block
[-> <a> [: label] [{ ... }] found <a> ->] [: label] lost
return [text] inside a block
destroy <a>
alt ["guard"] { ... } [else ["guard"] { ... }]...
par ["guard"] { ... } [and ["guard"] { ... }]...
opt|loop|break|critical|neg ["guard"] { ... }
ref over <a>[, <b>]... : <name>
note over <a>[, <b>]... : text note left of <a>: text note right of <a>: text
Participants must be declared before use. ; separates statements outside labels. A label
extends to the end of the line, except for a comment after whitespace (# or //) or a
space-separated { or }; quote a label to keep these as text. See
Sequence diagrams.
State diagrams
diagram state ["Title"]
direction right|down
state <name> [as <alias>] [<<stereotype>>] [{ ... }]
region ["name"] { ... } inside a state
entry|do|exit [/ expression] inside a state, one per line
trigger {, trigger} [guard] / effect internal transition, one per line
[*] --> a a --> [*] a --> b [: label] b <-- a
[H] [H*] Name[H] Name[H*] shallow and deep history
choice|junction|fork|join|terminate <name>
entrypoint|exitpoint <name> inside a state
note <target>: text
a left-of|right-of|above|below b
align a b c
A transition label has the form trigger {, trigger} [guard] / effect, with every part
optional. ; separates statements except internal behaviour lines and labels. A label extends
to the end of the line, except for a comment after whitespace. See
State diagrams.
Activity diagrams
diagram activity ["Title"]
start first statement of the flow
action|send|accept|object "name"
if "guard" { ... } [else if "guard" { ... }]... [else ["guard"] { ... }]
switch ["subject"] { case "guard" { ... } case "guard" { ... } ... }
while "guard" { ... }
repeat { ... } until "guard"
fork { ... } and { ... } [and { ... }]...
lane "name" { ... }
note: text
stop activity final
end flow final
A note attaches to the statement before it. Guards are written without brackets. In a diagram with lanes,
actions, signals and objects must be inside a lane block. See Activity diagrams.
Layout hints
drumlin normally places everything for you. Hints adjust the result and keep the model readable. They describe relations, never coordinates. Hints work in component, package and state diagrams. Sequence and activity diagrams take their order from the file.
| Hint | Effect |
|---|---|
a left-of b, a right-of b | Horizontal order |
a above b, a below b | Vertical order |
align a b c | Places elements in the same rank |
Same rank
Connectors normally give each element its own rank. align overrides that and
places elements in one rank, which shortens a wide diagram.
diagram component "Without align"
component Gateway
component Auth
component Orders
Gateway --> Auth
Auth --> Orders
diagram component "With align"
component Gateway
component Auth
component Orders
Gateway --> Auth
Auth --> Orders
align Auth Orders
Forcing order
With direction right, above and below order elements within a rank.
diagram component "Metrics above Logs"
component App
component Logs
component Metrics
App --> Logs
App --> Metrics
Metrics above Logs
Along the diagram direction, a hint forces a rank order even with no connector.
diagram component "Order without connectors"
component Boot
component Init
component Run
Boot left-of Init
Init left-of Run
Hints across containers
A hint between elements in different containers applies to the containers that hold them.
package "Ingest" { component Reader }
package "Serve" { component Query }
package "Admin" { component Console }
Reader left-of Query
Console left-of Query
Errors
Errors have the form file:line:col: error: message. The output below is real:
it was produced by drumlin during this site’s build, without the file prefix.
Unknown names
drumlin suggests the closest declared name.
component Scheduler
component Worker
Scheduelr --> Worker
drumlin says:
3:1: error: unknown element `Scheduelr`; did you mean `Scheduler`?
Duplicate declarations
component Worker
component Worker
drumlin says:
2:11: error: `Worker` is already declared
Unclosed blocks
component Outer {
component Inner
drumlin says:
2:1: error: missing `}` for `Outer`
Unmatched braces
component Lonely
}
drumlin says:
2:1: error: unmatched `}`
Invalid nesting
diagram package
package Outer { class Inner }
Outer +-- Inner
drumlin says:
3:1: error: `Inner` is drawn inside `Outer`, so containment needs no line; declare `Inner` outside the `Outer` block
Misplaced statements
provides IFoo
drumlin says:
1:1: error: `provides` and `requires` belong inside a component
Ambiguous ports
component A { port data }
component B { port data }
component C
data --> C
drumlin says:
4:1: error: port `data` is ambiguous; write `<component>.data`
Unimplemented diagram kinds
diagram timing "Handshake"
drumlin says:
1:1: error: diagram kind `timing` is not implemented; this prototype supports `component`, `package`, `sequence`, `state`, `activity`
Sequence diagrams
Names must be declared before use, so a typo is caught where it happens.
diagram sequence
participant Server
Servre -> Server : ping
drumlin says:
3:1: error: unknown participant `Servre`; did you mean `Server`?; declare it with `participant` before its first use
A return needs a message block to return from.
diagram sequence
participant A
return done
drumlin says:
3:1: error: `return` belongs inside a message block
A fragment continuation must match its opener.
diagram sequence
participant A
participant B
opt "x" {
A -> B : y
} else {
B -> A : z
}
drumlin says:
6:3: error: `else` must follow the `}` of an `alt`
State diagrams
Rules from UML 2.5.1 clause 14 are checked. A fork, for example, needs one incoming and at least two outgoing transitions.
diagram state
fork f
A --> f
f --> B
drumlin says:
2:6: error: fork `f` needs exactly one incoming and at least two outgoing transitions
A name used only once, close to another name, is reported as a likely typo. Write
state Idel first if the state really has that name.
diagram state
state Idle
[*] --> Idle
Idle --> Idel : wait
drumlin says:
4:10: error: unknown state `Idel`; did you mean `Idle`? declare it with `state Idel` if intended
A transition leaving a pseudostate cannot have a trigger.
diagram state
choice c
A --> c
c --> B : go
drumlin says:
4:7: error: a transition leaving a pseudostate cannot have a trigger; use `[guard] / effect`
A transition cannot join two orthogonal regions of one state.
diagram state
state A {
region {
[*] --> B
}
region {
[*] --> C
C --> B
}
}
drumlin says:
8:5: error: a transition cannot join two orthogonal regions of `A`
Activity diagrams
Statements after the end of a flow can never run.
diagram activity
start
stop
action "Late"
drumlin says:
4:1: error: this statement can never run: the flow already ended with `stop` on line 3; remove it, or move it before the end
Every branch of a fork needs a step.
diagram activity
start
fork {
action "A"
} and {
}
stop
drumlin says:
5:3: error: this `fork` branch is empty; each branch needs a step, or remove the branch
A flow must not end on a merge, because a merge has one outgoing flow.
diagram activity
start
if "x" {
action "A"
}
drumlin says:
3:1: error: the flow continues after this `if` but nothing follows; add a step, or end it with `stop` or `end`
When a diagram uses lanes, every action must be in one.
diagram activity
lane "A" {
}
start
action "B"
stop
drumlin says:
5:1: error: this diagram uses lanes, so this `action` must be inside a `lane` block
Drawing principles
drumlin aims to draw like a careful draftsman: someone who knows the notation, works on squared paper with one pen, and redraws a line rather than leave it crooked. The drawing should look deliberate. Every line is straight unless it has a reason to turn, every label sits where it can belong to only one thing, and the space between shapes is even.
This part of the book states that aesthetic as explicit rules. Each rule says what drumlin does now or what it should do, so that a change to the layout can be judged against it.
The state diagram above follows the rules. It flows one way, from top to bottom. The lines are
orthogonal, and none of them crosses another. Each label sits beside its own line. The reverse pair
between Running and Paused runs as two parallel lines, with their labels on the outside. The one
line that turns, stop, turns because three lines share the bottom of Running.
How rules are written
Each principle has:
- a name, in the imperative, such as Avoid crossings;
- the rule itself;
- why it matters;
- a source: the aesthetics research report in the repository (
docs/research/diagram-aesthetics.md), the studies it cites (Purchase 1997 and 2002, Ware et al. 2002, Gansner et al. 1993, Wybrow et al. 2009), the UML 2.5.1 specification, or drafting practice; - its status in drumlin:
- enforced: drumlin does this in every diagram it lays out, and the named stage of How layout works is where;
- partial: drumlin does this in some diagram kinds or cases, or tries but does not guarantee it;
- planned: drumlin does not do this yet. The Layout levers study measures what it would gain, where that was measured.
Where it helps, a rule shows a good drawing, rendered by drumlin, and describes or sketches the bad one.
When rules conflict
Rules conflict all the time. A label wants the straight line’s gap, which another line wants too. Growing a box straightens a connector but makes a row uneven. drumlin settles each conflict in this order, highest first:
- Correct notation. The UML 2.5.1 notation is never traded for looks. A package with contents keeps its name in the tab, even where a line would run straighter without the tab.
- Unambiguous reading. Every line, label and name must belong visibly to one thing. A label floating between two lines, an arrowhead on a corner, or two connectors leaving one point together are worse than any jog.
- No overlaps. No shape on a shape, no line on a line, and no line through a shape or a name. Two lines on one lane cannot be told apart, so this ranks above crossings, as the nudging stage ranks it.
- No crossings. Crossings matter most in every human-factors study of graph drawing (Purchase 1997; 93% preference in Purchase et al. 2002).
- Few bends. Bends come next in the preference data (91%), and continuity of a path helps it be traced (Ware et al. 2002).
- Flow and alignment. A single direction, ranks, centred and symmetric placement.
- Compactness. Small area and short lines, but never at the cost of the rules above.
- Uniformity. Even box sizes and even spacing. This is the first rule to give way: a box may grow to straighten a line.
The order follows the research summary: crossings first, then bends, with orthogonality held throughout by construction. Readability and notation sit above everything, because a drawing that reads wrongly is worse than one that reads slowly.
The pages
- Shapes and spacing: grid, line weight, sizes and box growth.
- Lines and routing: orthogonal lines, bends, crossings, overlaps, sides, attachment points, channels and arrowheads.
- Labels: closeness to their line, no ambiguity, and never forcing a bend.
- Names and titles: where names go, and when they give way to lines.
- Flow and arrangement: direction, ranks, alignment, symmetry and grouping.
- Notes per diagram kind: where sequence, activity and the others differ.
- Layout levers: the measured study behind the planned rules.
Shapes and spacing
Snap to the grid
Rule. Every box corner, connection point and line segment lies on an 8px grid. Box sizes are multiples of 16px.
Why. A grid makes alignment the default instead of an accident: two boxes of one height in one rank share their top, centre and bottom, and parallel lines keep whole-step distances. It is the squared paper of hand drafting.
Source. Drafting practice; DECISIONS.md, “Format, look and layout control”.
Status: enforced. Placement snaps positions, slots and lanes snap in Slots and Routing. Reserved label boxes are the exception: their text is measured, so their edges need not lie on the grid.
Use one pen
Rule. All outlines and lines use one ink colour (#1f2933), one stroke width (1.25) and a white
fill. Dashes mean a dependency or comment, never emphasis. Secondary text, such as labels and
keywords, is smaller and grey. It is never a second weight of line.
Why. One weight keeps the drawing quiet. Differences then carry meaning: a dashed line is a different relation, not a different mood. Purchase et al. (2002) found that 61% preferred diagrams without font variation.
Source. Drafting custom, Tufte’s smallest effective difference (both via the research report); Purchase, Allder and Carrington 2002.
Status: enforced, by the shared stylesheet in core::svg.
Distinguish outlines from connectors
Rule. Outlines could be slightly heavier than connectors, at most 1:2 (for example 1.5 and 0.75), as ISO 128 separates thick and thin lines.
Why. The eye then separates things from relations at once.
Source. ISO 128-24 (via the research report, recommendation 6).
Status: planned. Not scheduled. It changes every golden file and needs the owner’s decision, since “one pen” is the current house style.
Size boxes to their text
Rule. A box is as wide as its name and keyword need, plus padding, and never smaller than the kind’s minimum (128 by 56 for a component, 96 by 40 for a state). Widths come from embedded Helvetica metrics, so a box is the same size on every machine.
Why. A box that fits its text reads as one unit. A fixed size either crowds long names or leaves short ones lost. Measured text keeps the layout deterministic.
Source. DECISIONS.md, “Font stack leads with Helvetica metrics”.
Status: enforced, by each adapter’s size function (leaf_size, state_size).
Leave room for what attaches
Rule. A side that carries several ends, ports or interface stubs is long enough to space them at least 24px apart, 16px clear of its corners. A state with many transitions is wider.
Why. Attachment points that crowd at a corner or touch each other make arrows unreadable, and force lines to jog to reach them.
Source. Drafting practice; Spread and order attachment points.
Status: enforced, by side_needs (component), slot_room (package) and the incident count in
state_size.
Keep gaps even and generous
Rule. Ranks are at least 64px apart and neighbours in a rank 40px apart. A gap grows when a label or interface needs it, and never shrinks below that.
Why. Even spacing reads as order. Generous gaps leave channels for lines to run in without crowding, which is cheaper than any later repair.
Source. Gansner et al. 1993 (separation in layered layout); drafting practice.
Status: enforced, by layered::Spacing and the edge gaps each adapter passes.
Grow a box rather than bend a line
Rule. When a connector jogs only because its two end boxes miss each other by a few grid steps, grow one box by up to two grid steps on a side, toward the other end, into free space, so that the line runs straight.
Why. Bends rank above uniform sizes (see When rules conflict). A box that is 16px wider costs almost nothing, while the jog it removes costs a turn in every reading of that line.
Good: Message Bus grows 8px at its top and bottom, and each target 8px toward it, so all three
connectors run straight. Before box growth, two of them jogged, because Message Bus was no taller
than Checkout and could not reach both rows.
diagram component "Lines straightened by a taller box"
component Checkout
component "Message Bus" as bus
component Payments
component Inventory
Checkout --> bus
bus --> Payments : OrderPlaced
bus --> Inventory : OrderPlaced
A box grows only into free space: 16px clear of every other box, inside its container’s padding, and clear of reserved labels, self-transition loops, interface stubs and ports. A box with stubs, ports or self-transitions keeps its size. Growth across the flow is tried before growth along it. Each growth is kept only when routing again shows the connector lost a bend, no total rose, and no two connectors came to leave one point together. Over the 103 golden files, box growth removes 80 bends after the corner inset and tab ends, and the two Compiler pipeline crossings. The bounds of the diagram do not change.
Source. Purchase et al. 2002 (bends 91%); Layout levers.
Status: enforced, by core::grow after layout, at up to two grid steps. Four steps, which the
study measured at another 38 bends, is not scheduled.
Keep a rank even
Rule. Boxes in one rank share a centre line along the flow, and should look like one row. When a box grows, prefer growth that keeps the row tidy, such as growing toward the gap between rows rather than away from it.
Why. Uneven heights make a row read as a staircase. This is the uniformity that box growth trades away, so it limits how far a box may grow.
Source. Drafting practice; the images in Layout levers.
Status: partial. Items in a rank are centred on one line (place_along). Box growth tries growth
across the flow first, which keeps a rank’s extent along the flow even, and grows at most 16px on a
side. A box may still end up a step taller or wider than its neighbours, as Kafka Adapter in
Hexagonal service or Mobile App in API gateway, and a box that grows on one side only has its name
up to 8px off its neighbours’ line.
Lines and routing
These rules apply to component, package and state diagrams, which share the layered layout and the router. Sequence and activity diagrams have their own layouts; see Notes per diagram kind.
Draw lines orthogonally
Rule. Every connector is a chain of horizontal and vertical runs with small rounded corners (6px radius, less on very short runs). There are no diagonals.
Why. Orthogonal lines match the grid and the boxes, cross at right angles, which readers barely notice (Huang), and ranked second in UML diagram preference (Purchase et al. 2002).
Source. Purchase, Allder and Carrington 2002; Huang 2008 (via the research report);
DECISIONS.md, “Format, look and layout control”. UML gives segments no meaning (“The individual
segments of the line itself have no semantic significance”, 11.5.4, p. 201), so the style is free.
Status: enforced. Routing searches only orthogonal moves, and
glyphs::rounded draws the corners.
Keep lines out of shapes and names
Rule. A line never passes through a box, a symbol, a name, a reserved label or an interface stub. It may cross a container’s border only to reach something inside it.
Why. A line through a shape reads as a connection to that shape. A line through text makes both unreadable.
Source. Castelló, Mili and Tollis 2000 (their motivating figure shows edges through states as the failure to avoid).
Status: enforced. Each connection carries its own obstacles in Routing, and Nudging never moves a segment through one.
Never lay lines on top of each other
Rule. Two different connectors never share a run. Lines may join only where the notation joins them: the branches of a containment tree from one circle-plus, or several connectors meeting at one port.
Why. Two lines on one lane cannot be told apart, and a shared start reads as one connector that forks. That is worse than a crossing, which is still readable.
Bad: two connectors leave A from one point and share a run, so they read as one line that forks.
Source. Wybrow, Marriott and Stuckey 2009, section 5; DECISIONS.md, “Nudging and sliding ends”.
Status: enforced. Nudging ranks overlaps above crossings, and the quality test’s overlap budget is 0. Ends that slide may not come closer than 16px. The study found that an 8px gap lets two ends meet, so the gap stays. The quality test also counts merged ends, two connectors leaving one point together, which the overlap count excuses; its budget is the two intended fan-outs from one port, and box growth is never kept if it merges two connectors.
Avoid crossings
Rule. Lines should not cross. When a crossing cannot be avoided, it is a clean right angle away from any corner, label or arrowhead.
Why. Crossings have the largest effect of any aesthetic on reading graphs (Purchase 1997), and 93% of subjects preferred the UML diagram with fewer (Purchase et al. 2002).
Bad: the lines to C and D cross, although swapping the order of their ends would separate them.
Source. Purchase 1997; Purchase, Allder and Carrington 2002.
Status: partial. Ordering (Crossing reduction), the router’s crossing cost and nudging remove most crossings. Three remain in the 103 golden files, all in component diagrams. Two (Order Platform) need ordering across containers, which the recursive layout cannot see. One (Hexagonal service) comes from attachment points ordered by the position of the other end rather than by the direction the line arrives from. Box growth removed the two Compiler pipeline crossings. See the five crossings.
Bend only when you must
Rule. A line turns only when its ends are not aligned and cannot be. A Z between two ranks is acceptable when the ends cannot share a coordinate; a jog that a slightly different layout would remove is a defect.
Why. Bends are second only to crossings in the preference data (91%), and a continuous path is traced faster (Ware et al. 2002).
Source. Purchase et al. 2002; Ware, Purchase, Colpoys and McGill 2002.
Status: partial. Straight long edges (Placement), slots that slide toward their peers (Slots) and jog folding (Nudging) keep most lines straight, and Box growth, the 8px corner inset and ends on package tabs remove 100 more. Since stage 10, a Z becomes an L where the L is clear, and state labels may follow their lines. 193 bends remain in the golden files, down from 341. About a quarter are necessary, at fans. The rest have causes that Layout levers lists one by one: labels, port policy, state names, layout order, and boxes that would need more than two grid steps.
Leave from the facing side
Rule. A connector leaves its box on the side that faces the other end. When the boxes are apart on both axes, the side along the flow wins.
Why. The line then takes the short way, and the flow direction stays visible at both ends.
Source. Purchase et al. 2002 (information flow ranked third for UML).
Status: enforced, by router::facing, used by every adapter.
Prefer an L to a Z when the L is clear
Rule. When two boxes are apart on both axes and an L-shaped line between a side and a top (or bottom) is clear of everything, use the L, with one bend, instead of a Z with two.
Why. It saves a bend without hiding the flow, as long as the last run is long enough for its arrowhead.
Source. Purchase et al. 2002 (bends).
Status: enforced for plain boxes. State diagrams do this for final states, choices, junctions and terminate symbols when choosing sides (side arrivals). After routing, L over Z moves one end of a Z to the side of its box that faces the line, when the L then meets no box, name, reserved label or boundary, keeps 16px from other ends, gains no crossing, overlap or close run, and merges no ends. It applies to leaf components without ports or stubs, leaf packages and classes (not a package’s top, which holds its tab), and simple states (not the sides of a state with internal behaviours, which its compartment line meets). In the golden files it removed 41 bends.
Spread and order attachment points
Rule. Ends on one side are spread at least 24px apart, in the order that keeps their lines from crossing, and each slides toward the line that makes its connector straight.
Why. Evenly spaced ends read as separate connectors. Their order decides whether lines cross just outside the box.
Source. Schulze, Spönemann and von Hanxleden 2014 (port constraints in layered layout, via the research report).
Status: partial. Slots spreads and orders ends by the position of the other end. Ordering by the side the route arrives from is not done, and this causes three of the five remaining crossings.
Keep ends off corners
Rule. An end stays at least 8px from a square box corner, and 16px from the curve of a rounded state corner. Ends are first spread 16px from corners; nudging may slide them to 8px on square boxes.
Why. An arrowhead at a corner is ambiguous about which side it meets, and on a rounded corner it meets a curve.
Bad: the arrowhead meets B in its rounded corner.
Source. Drafting practice; Layout levers (images of an 8px inset).
Status: enforced: 8px on square component and package boxes (CORNER_INSET in their routing),
16px on states. The 8px inset saved 12 bends and merges nothing. At 8px on states, whose corners have a
10px radius, the arrowhead would land on the curve. The 16px gap between ends stays.
Keep arrowheads apart
Rule. Two ends on one side are at least 16px apart after nudging, and 24px when slots are first spread.
Why. Two arrowheads closer than their own width merge into one mark.
Source. DECISIONS.md, “Nudging and sliding ends” (SLIDE_GAP).
Status: enforced, by router::SLIDE_GAP and the slot spacing in each adapter.
End with a straight run
Rule. Every line leaves and enters a box with a straight run of at least 16px, so the arrowhead sits on a straight piece and points squarely at the side.
Why. An arrowhead on a corner points in two directions at once.
Source. Drafting practice.
Status: enforced, by the 16px stems of router::Terminal and, at state pseudostates, the 16px
ARRIVAL_RUN.
Space parallel lines evenly
Rule. Lines that share a channel are spread evenly across it, at least 8px apart, and a lone jog sits in the middle of its gap.
Why. Wybrow et al. report that this nudging step has “a significant impact on the perception of layout quality”. Even spacing looks deliberate. Lines bunched on one side look accidental.
Source. Wybrow, Marriott and Stuckey 2009, section 5.
Status: enforced. The spread pass of Nudging does this.
Do not run along a boundary
Rule. A line never travels along a container’s border. It crosses the border at a right angle.
Why. A line along a border merges with it, and reads as part of the container.
Source. Drafting practice.
Status: enforced. Container borders are soft strips in Routing, and Nudging never moves a segment onto one.
Join lines only where the notation does
Rule. Containment drawn with branching lines has one trunk from the owner’s circle-plus. Other relations each get their own line, even when they share an end.
Why. A shared trunk says “these belong together”, which is true for containment (UML 12.2.4) and false for, say, two dependencies.
Source. UML 2.5.1, 12.2.4, p. 248; Purchase et al. 2002 (76% preferred joined inheritance arcs, where the notation joins them).
Status: enforced. Package containment draws a tree when every member sits in the next rank, and otherwise routes each branch from the shared anchor.
Put arrowheads where the notation says
Rule. Arrowheads, triangles, circles and sockets follow UML 2.5.1 for each relation: an open head for a dependency, a hollow triangle for generalization, a filled head for a synchronous message, the circle-plus on the owner of a containment.
Why. The notation is the meaning, so it is never traded for looks. See When rules conflict.
Source. UML 2.5.1; docs/research/uml-audit.md in the repository.
Status: enforced, by the notation audit of stage 6a and the glyphs in core::glyphs.
Labels
Labels here are the text on lines: connector labels, keywords such as «import», transition triggers
and guards, message names. Names of boxes and containers are in Names and titles.
Keep a label beside its own line
Rule. A label sits next to a straight run of its own line, 6 to 8px from it, and close to the middle of that run.
Why. Closeness is what ties a label to its line (Gestalt proximity). A label far from its line has to be matched by searching.
diagram state "Labels beside their lines"
[*] --> Idle
Idle --> Busy : start [ready] / log()
Busy --> Idle : done
Source. Gestalt proximity (via the research report); Kakoulis and Tollis 1997.
Status: enforced. A reserved state label sits 8px from its anchor
(Placement), and a state label placed after routing sits with its
ink 8px from its own segment. labels::Placer tries five positions along each segment, 6px off it, and
prefers the middle and longer runs. The quality test counts labels more than 12px from their own line:
4 state labels in the golden files: two-line labels above a line in a rightward flow, whose upper
line is 14px away, and a self-transition label in the staircase under its loop.
Never leave a label between two lines
Rule. No other line may be as near to a label as its own line. A label that sits between two parallel lines must move, or one of the lines must.
Why. A label equally near two lines belongs to neither, and a reader guesses. This is the unambiguous reading rule, second only to notation in When rules conflict.
Bad: retry could belong to either line.
Source. Kakoulis and Tollis 1997 (a label must be associated with exactly one edge); drafting practice.
Status: partial. The quality test counts every connector label with another line at least as near as its own: 1 of 118 state labels, 1 of 27 package labels and none of 69 component labels in the golden files. A state label placed after routing must be 12px nearer its own line than any other, or it keeps its reserved spot. Component and package labels are placed by fewest collisions and are not checked for this when placed.
Never let a label touch other ink
Rule. A label does not overlap a box, a name, another label or any line.
Why. Text over ink is hard to read, and a label on a line seems to belong to it.
Source. Purchase et al. 2002 (text legibility); Castelló, Mili and Tollis 2000.
Status: partial. Reserved state labels are obstacles for every line, so no line crosses them, and a state label placed after routing must clear every line, outline, arrowhead, shape and label. The placer used for component and package labels takes the position with the fewest collisions, but cannot make room when every position collides. The quality test counts labels within 2px of a line or an outline, or on a shape or label: 1 in the golden files (a package label).
Write labels horizontally
Rule. All text runs left to right, horizontally, including labels on vertical lines.
Why. 86% of subjects preferred horizontal labels in UML diagrams.
Source. Purchase, Allder and Carrington 2002.
Status: enforced. No text is rotated.
Put labels on a consistent side
Rule. In a diagram that flows down, a label sits right of its vertical line. In one that flows right, it sits above its horizontal line. A reverse pair between two items keeps both labels on the outside, so the two lines run close together.
Why. A consistent side lets the eye find each label without searching, and keeps lines and labels from interleaving.
Source. Drafting practice; DECISIONS.md, “State machine diagrams (stage 4)”.
Status: enforced for state diagrams (label_side, order_label_sides). Component labels go right of
vertical runs and above horizontal runs. Package labels may also take the left of a vertical run, to
avoid a collision.
Reserve room for labels
Rule. The layout makes room for long labels before lines are routed, instead of hoping that a spot will be free afterwards.
Why. A placer that chooses among positions cannot make space, and fails on dense diagrams. Label dummies that take part in the layout guarantee room (dot, KLay).
Source. Gansner et al. 1993; Schulze, Wechselberg and von Hanxleden 2018; Castelló et al. 2000 (all via the research report).
Status: partial. State transition labels are dummies in the layered layout (Placement). Component and package diagrams flowing right widen the rank gap by the label’s width. Elsewhere labels are placed after routing.
Never let a label force a bend
Rule. A label follows its line, not the reverse. A line should run as straight as it would without its label, and the label then takes a free spot beside it. When no spot is free, the label keeps its reserved place, and the line goes through it.
Why. Labels serve lines. Today a state transition is routed through its label’s anchor, and every
label is an obstacle for every line, so a short transition jogs just to pass its anchor. In Elevator
controller, near target floor, speed zero, levelled at floor and fully open all do this.
Source. Purchase et al. 2002 (bends); Layout levers.
Status: partial, for state diagrams (Labels after routing).
The layout still reserves every label’s room. Each line that bends is routed again without its waypoint,
and its label placed beside the new line, on the segment and side that read best. A label keeps its
reserved spot when no place is clear, near (12px) and unambiguous, when its line gains no straightness,
or when its line gains a crossing. A diagram keeps the fully reserved drawing unless the result has
fewer bends and no more crossings, overlaps, merged ends or label flaws. The study measured 26 bends for
this before box growth; after box growth and L over Z it saves 7, because most short jogs past an anchor
are already gone. The labels freed in the golden files are motor overcurrent / cut power (Elevator),
close / send FIN (TCP) and wait(event) / block() (Process lifecycle). near target floor keeps its
spot: right of its straightened line it would lie on next call, and left of it on the region border.
Names and titles
Names are the text that identifies a shape: a component’s or state’s name, a package’s name in its tab, the name beside an entry point. The diagram title is a caption.
Centre a classifier’s name, keyword above
Rule. A component, class, interface or other classifier shows its name centred in bold, with any keyword or stereotype centred in plain text above it, in guillemets. This includes a composite component, whose name is centred in its header.
Why. UML asks for it, and a centred name marks the box as one unit.
Source. UML 2.5.1, 9.2.4.1, p. 101: “The name of the Classifier should be centered in boldface”, with keywords “centered in plain face within guillemets above the Classifier name”.
Status: enforced in component and package diagrams. See DECISIONS.md, “UML 2.5.1 audit”.
Put a package’s name in its tab when it shows its contents
Rule. A package that shows its members carries its name, left-aligned, in the tab. A package without visible contents has a blank tab and its name in the body.
Why. UML specifies both cases. Readers find the name in the same place in every package.
diagram package "Names in tabs"
package Core {
class Order
class Invoice
}
package Util
Core ..> Util : <<import>>
Source. UML 2.5.1, 12.2.4, p. 248.
Status: enforced, in package diagrams and for packages inside component diagrams.
Keep lines off names
Rule. No line crosses a name, a keyword, a composite state’s internal activities, or a package tab.
Why. A line through text hides both. A line through a tab reads as entering the tab, which means nothing.
Bad: the arrow pierces Util’s tab to reach the body below it.
Source. Keep lines out of shapes and names.
Status: enforced. Composite names, state headers and tabs are obstacles for every line that enters their container. The study found that, as obstacles, they cost no bend in any golden file.
Let arrows end on a package’s tab
Rule. A relation arriving at a package from above may end on the top of the tab, when that is where its straight line meets the package.
Why. The tab is part of the package symbol, so a line ending there points at the package. If the top side started right of the tab, a line that would arrive over the tab would jog sideways instead.
Source. UML 2.5.1, 12.2.4, p. 248 (the tab is part of the symbol); Layout levers.
Status: enforced for packages without visible contents, whose tab is blank (tab_takes_ends in the
package routing). An end on the tab stays 8px from its corners and 16px from the step down to the
body. It saved 8 bends in the golden files, in language-relations, language-cycle and
gallery-merge. A package showing its contents keeps its name in the tab, so its top side still starts
right of the tab.
Let a name give way to a line
Rule. Where the notation leaves the name’s position free, move the name aside instead of bending a line. A composite state’s name may sit left or right in its name compartment, or in a UML name tab, when an entry point or a straight line wants the middle. A classifier’s name stays centred, because 9.2.4.1 asks for it.
Why. The name can move without loss. The line cannot bend without cost. Today an entry point slides off the centred name instead, and the transition into it then jogs.
Source. UML 2.5.1, 14.2.4.4, p. 319-320, which allows an optional name tab “resting on the outside of the top side of a State”; Layout levers.
Status: planned. Small gain: 2 of the 11 composite state names in the golden files would move, saving 4 bends.
Keep connection point names beside their symbols
Rule. An entry or exit point’s name sits next to its circle, clear of the state’s own name, and the entry point itself slides along the border off that name.
Why. Two names overlapping cannot be read, and a connection point name far from its circle is easily taken for the state’s.
Source. UML 2.5.1, 14.2.4.6, p. 327-328 (entry and exit points as circles on the border, “with the name associated with it”).
Status: enforced (clear_of_name in the state layout).
Caption the diagram, do not frame it
Rule. A diagram kind "Title" title is a caption above the drawing, outside its contents area.
There is no UML frame.
Why. A frame adds a box around every drawing and competes with the content. UML makes the frame optional, and drops its heading with it.
Source. UML 2.5.1, Annex A; DECISIONS.md, “Frames, gates and coregions stay undrawn”.
Status: enforced, by core::svg::document.
Flow and arrangement
Flow one way
Rule. A diagram has one main direction, down or right, and most connectors point along it. Items sit in ranks across that direction, and a connector joins items in later ranks wherever possible.
Why. A single direction lets the reader follow cause to effect without turning the page. Purchase et al. ranked information flow third among UML layout preferences.
diagram component "One direction"
direction right
component Parser
component Checker
component Emitter
Parser --> Checker : AST
Checker --> Emitter : typed AST
Source. Purchase et al. 2002; Sugiyama et al. 1981 and Gansner et al. 1993 (layered drawing).
Status: enforced. Ranking assigns ranks. Component diagrams flow
right by default, and package and state diagrams flow down, because their long labels then sit beside
vertical lines. direction changes it.
Keep connected items close
Rule. Connected items sit in neighbouring ranks where the graph allows. A source pulls next to the first thing it feeds.
Why. Short edges need fewer dummies, fewer bends and less searching.
Source. Gansner et al. 1993 (minimising weighted edge length).
Status: partial. Longest-path ranking with sources pulled next to their targets. The optimal network-simplex ranking of the research report (recommendation 4) is not implemented.
Order ranks to avoid crossings
Rule. Within each rank, items are ordered so that edges between ranks cross as little as possible. Hints and notes are part of the ordering, not applied afterwards.
Why. Most crossings are decided by the order, before any line is routed.
Source. Gansner et al. 1993 (weighted median, transpose); research report recommendation 2.
Status: enforced within a container (Crossing reduction).
Order across containers too
Rule. The order of items inside a container should take the lines that leave the container into account.
Why. A container laid out on its own can put a child on the wrong side of its outside partners. The Order Platform diagram has its one crossing for this reason, and the Compiler pipeline has a jog between its two containers.
Source. Sander 1996; Forster 2002 (via the research report); Layout levers.
Status: planned. Not scheduled. It needs a global layering across the hierarchy, which is a rewrite
of arrange.
Keep long edges straight
Rule. An edge that spans several ranks runs as one straight line, where both ends can take it. Where only one end can, it bends once, near the other end.
Why. Long edges that wander become spaghetti (Gansner et al.), and continuity helps path tracing (Ware et al.).
Source. Gansner et al. 1993; Ware, Purchase, Colpoys and McGill 2002.
Status: enforced, by the straightening step of Placement.
Align a pendant with what it hangs from
Rule. An item joined to the rest by a single chain, such as an initial state, a final state or a note-like leaf, sits in line with the item it hangs from, so the chain is straight.
Why. A short spur that jogs looks careless, and it is the easiest jog to remove.
diagram state "Pendants in line"
[*] --> Ready
Ready --> Done : finish
Done --> [*]
Source. Drafting practice.
Status: enforced, by the pendant step of Placement.
Centre and balance
Rule. An item with several neighbours in the next rank sits centred over them. A rank is balanced around the items it connects to, and the layout is symmetric where the graph is.
Why. Symmetry has a measurable but smaller effect than crossings and bends (Purchase 1997), and it is what makes a tree look like a tree.
Source. Purchase 1997; Gansner et al. 1993; Brandes and Köpf 2001.
Status: partial. An order-preserving median solve aligns connected items. It is a heuristic: an item can sit off the line of what it connects to. Network simplex or Brandes-Köpf coordinates are not implemented (research report recommendation 4).
Group what belongs together
Rule. Containers hold their contents. Notes sit beside what they annotate. Unconnected siblings fold into a compact block instead of one long row. Orthogonal regions tile across the flow.
Why. Proximity groups things before any line is read.
Source. Gestalt proximity (via the research report); UML 2.5.1, 14.2.4 (regions).
Status: enforced: Compound layout, the Beside and
Adjacent constraints for notes, wrap_constraints for package siblings, and region tiling in the
state layout.
Honour hints over heuristics
Rule. A layout hint (left-of, above, align and the others) always wins over what the heuristics would choose.
Why. The author knows what the diagram is for. A hint is the one way to say so without coordinates.
Source. DECISIONS.md, “Format, look and layout control”; Layout hints.
Status: enforced. Constraints apply inside every ordering sweep and win over edges.
Draw the same input the same way
Rule. The same source always gives the same drawing, byte for byte, on every machine.
Why. A diagram in version control should change only when its source does, and readers should not have to find their way around a drawing again after an unrelated edit.
Source. PLAN.md rules; the D2 TALA author’s caution about seed-dependent layouts (via the
research report).
Status: enforced. There is no randomness, no HashMap order in the output, and text is measured with embedded metrics. The golden tests check it.
Notes per diagram kind
The general rules hold everywhere. This page lists where a diagram kind adds rules or bends them. Component, package and state diagrams share the layered layout and router. Sequence and activity diagrams have layouts of their own, built so that most line rules hold by construction.
Component diagrams
Flow right, and leave room for the wiring
Rule. Component diagrams flow right by default. A rank gap grows to fit a connector’s label, and an assembly or wired connector’s ball, socket and arrow.
Why. Interfaces and their names sit along horizontal runs, where they have room.
Source. DECISIONS.md, “UML 2.5.1 audit (stage 6a)”.
Status: enforced (edge_spec in the component layout).
Keep a delegation straight, and let the outer connector take the jog
Rule. A port’s position on its side follows the connectors inside the component first, so the delegation is straight. The connector outside takes any jog.
Why. Inner gaps are narrow, so a jog there crowds the contents. Outside there is room.
Source. DECISIONS.md, “Shape of the generic layout and routing APIs”.
Status: enforced, but it is a choice with a cost: 32 bends in the golden files are outer connectors jogging to a port that sits where the inner line wants it. Letting the outer line be straight when the inner one has room is a lever noted in the study.
Draw ball and socket clear of names
Rule. The ball, the socket and the interface name sit on a straight run of the connector, clear of other labels and names.
Why. The glyph is notation. A name over it hides what it says.
Source. UML 2.5.1, 11.2.4 and 10.4.4.
Status: enforced (draw_assembly and draw_wired choose a free straight run).
Package diagrams
Draw containment as one tree
Rule. When every member sits in the next rank, an owner’s members hang from one trunk with the circle-plus on the owner. Otherwise each branch is routed from that shared anchor.
Why. UML draws membership as branching lines from one circle-plus (12.2.4).
Source. UML 2.5.1, 12.2.4, p. 248.
Status: enforced.
Fold unrelated siblings into a block
Rule. Four or more siblings with no relations or hints between them fold into a roughly square block of rows.
Why. One long row of unrelated packages wastes width and suggests an order that is not there.
Source. DECISIONS.md, “Package diagrams (stage 2)”.
Status: enforced (wrap_constraints).
State diagrams
Reserve label room and keep labels on the flowing side
See Reserve room for labels and Put labels on a consistent side. State diagrams are where labels matter most (Castelló et al.), and where they currently cost the most bends: Never let a label force a bend.
Draw self-transitions as compact loops away from the flow
Rule. A self-transition is a small loop on the side of the state away from the flow, with its label beside it. A hidden box keeps the state centred in its rank.
Why. A loop on the flowing side would collide with the transitions in and out.
Source. DECISIONS.md, “State machine diagrams (stage 4)”.
Status: enforced.
Meet small symbols on their sides
Rule. A line into a final state, choice, junction or terminate symbol from a source that cannot line up with it meets the symbol’s facing side, as an L. Lines out of a choice fan from different corners.
Why. Lines meeting at one point of a small symbol merge into one ambiguous junction.
Source. UML 2.5.1, Figure 14.25; DECISIONS.md, “State pseudostates take lines on their sides”.
Status: enforced.
Tile regions across the flow
Rule. Orthogonal regions sit side by side across the flow, with equal extent along it, separated by dashed lines.
Why. This matches Figures 14.9 and 14.10, and lets each region keep the diagram’s direction.
Source. UML 2.5.1, 14.2.4.3 and Figures 14.9 and 14.10.
Status: enforced.
Sequence diagrams
Sequence diagrams use a timeline layout: lifelines are columns, events are rows on the 8px grid, and there is no router.
Never cross a lifeline with text
Rule. A message label stays within one lifeline gap, the gap nearest the sender that fits it, and the columns widen until it fits.
Why. Text across a lifeline is cut by it, and a reader cannot tell which side it belongs to.
diagram sequence "Labels between lifelines"
actor User as u
participant API as a
participant DB as db
u -> a : place order
a -> db : insert(order)
db --> a : id
a --> u : confirmation
Source. DECISIONS.md, “Sequence diagrams (stage 3)”.
Status: enforced. The quality test counts any label within 2px of a lifeline: none in the goldens.
Keep labels off arrowheads and dots
Rule. A message label keeps 4px from the arrowhead it points at. A found or lost message’s stub is long enough to hold its label between the dot and the arrowhead, clear of both, and a creation message’s gap counts the new head’s half width.
Why. Text that touches the arrowhead reads as part of it, and the halo bites into the head.
Source. UML 2.5.1, 17.4.4.1 and Figure 17.11 (lost and found messages); DECISIONS.md, “Sequence and
activity quality (stage 9)”.
Status: enforced since stage 9; the quality test counts labels within 2px of an arrowhead or dot.
Keep frame tabs and notes off execution bars
Rule. A fragment’s operator tab, or a note across lifelines, should not hide part of an execution bar.
Why. A bar cut by a tab or a note looks like an execution that stopped and started again.
Source. UML 2.5.1, 17.2.4 and 17.6.4.
Status: planned. Tabs sit at the frame’s corner, which is 12px left of the first lifeline’s bar, so a tab over an active bar hides a stretch of it; eleven such cases remain in the goldens and are counted.
Keep messages horizontal and time downward
Rule. Every message is a horizontal arrow, and later events are lower. Nested executions step right.
Why. That is the notation (17.4.4). Horizontal arrows keep the time order unambiguous.
Source. UML 2.5.1, clause 17.
Status: enforced. Consecutive one-line messages are 32px apart; the quality test counts any other gap between them with nothing drawn in between.
Activity diagrams
Activity diagrams use a structured-flow layout driven by the block tree, so lines are straight by construction and nothing is routed by search.
Keep the main flow on one axis
Rule. The flow that continues takes the straight axis. Side arms branch off and rejoin beside it, and a bypass runs on the right.
Why. The reader follows the main path down a straight line, and the exceptions read as detours.
Below, the [no] arm stops, so Ship and Invoice stay on the axis.
diagram activity "Main flow straight"
start
action "Receive order"
if "in stock" {
action "Ship"
} else "no" {
action "Cancel"
stop
}
action "Invoice"
stop
Source. DECISIONS.md, “Activity diagrams (stage 5)”; Ware et al. 2002 (continuity).
Status: partial. The axis stays straight, but the statement after a branch whose siblings stop waits
below the tallest sibling, so the drop to Invoice is longer than the others. The quality test counts
such drops as uneven gaps.
Space a straight flow evenly
Rule. Consecutive nodes in a straight flow are 32px apart. A guard beside a drop adds the room it needs.
Why. Even spacing makes a straight flow read as one sequence, and a longer gap then signals something.
Source. DECISIONS.md, “Sequence and activity quality (stage 9)”.
Status: partial. Exceptions in the goldens: a while loop’s merge and decision sit 24px apart and read as one test; a note rising over a decision lengthens the drop into it; a node with a taller note is centred in the note’s height; and the continuation after a branch whose siblings stop (above).
End note links on the shape
Rule. A note’s dashed link ends on the outline of the node it annotates.
Why. A link that stops short in white space does not say which node it means.
Source. UML 2.5.1, 7.4.4 (a note is attached by a dashed line); DECISIONS.md, “Sequence and activity
quality (stage 9)”.
Status: enforced since stage 9: a note above a decision now reaches the diamond’s outline.
Put guards by the branch they guard
Rule. A guard sits at the start of its flow, by the corner where the flow leaves the decision. Arms that share one run keep their guard over their own drop.
Why. A guard read near the decision is matched to the right arm.
Source. UML 2.5.1, 15.2.4 (guards on outgoing flows); DECISIONS.md, “Activity diagrams (stage 5)”.
Status: enforced. The quality test counts guards touching a flow, node, divider or other label: none.
Draw a merge only where flows rejoin
Rule. A merge diamond or join bar is drawn only where at least two flows meet.
Why. A merge with one input is ink without meaning.
Source. UML 2.5.1, 15.3.3; DECISIONS.md, “Activity diagrams (stage 5)”.
Status: enforced.
Layout levers: what each one would save
Written 2026-11-09 against tip da244717ddf3 (end of stage 7). Every number below was measured, not
estimated, over the 103 component, package and state goldens with the metrics of core::metrics
(crossings, overlaps, bends), the same counts the quality test prints. The baseline is
5 crossings, 0 overlaps, 341 bends (component 5/0/148, package 0/0/34, state 0/0/159).
The project owner observed three things in the rendered diagrams:
- (a) edge labels force bends that are not needed;
- (b) container and composite names sometimes block lines that could run straight if the name moved;
- (c) some boxes could be wider or taller, so that a connector runs straight instead of jogging.
Each was tested as a counterfactual. The experiment code was a thread-local switch set read by the three adapters, plus an analysis test that ran every variant and wrote the SVGs for inspection. It was not committed: no layout behaviour and no golden file changed.
Summary
| Lever | Upper bound | Realistic variant | Crossings | Area cost | Effort | Risk |
|---|---|---|---|---|---|---|
| Box growth, up to 4 grid steps | — | −128 bends | 0 | +3.1% of box area, no growth of the diagram | Medium (2–3 days) | Low to medium: uneven box sizes |
| Box growth, up to 2 grid steps | — | −90 bends | 0 | +1.6% of box area | Medium | Low |
| Box growth, 1 grid step | — | −56 bends | 0 | +0.9% of box area | Medium | Low |
| Corner inset for ends (attachment) | −22 bends, −1 crossing, but 3 merged ends | −12 bends | 0 | none | Very small (hours) | Low |
| Titles (names and tabs) | −12 bends | −10 bends | 0 | none | Small | Low (tabs), medium (state names) |
| Labels not forcing bends (state) | −59 bends, +3 crossings | −26 bends | 0 | none | Large (3–5 days) | Medium to high |
| Labels, no reservation at all | −69 bends, +8 crossings | not usable | +8 | −20% diagram area | — | High |
| All realistic levers but labels | 3 crossings, 0 overlaps, 207 bends | −2 | as box growth | |||
| All realistic levers | 3 crossings, 0 overlaps, 185 bends | −2 | as box growth |
“Upper bound” removes the cause outright, which is not always acceptable to draw. “Realistic” is a variant that keeps every rule in the principles: no label touches ink or reads ambiguously, no arrow pierces a name, no two connectors merge. Each realistic variant was also checked per diagram: where it made any count worse, the diagram kept its baseline. Only the label variant needed this. In three state diagrams it is worse on its own, and with the check the label lever saves 26 bends rather than 11.
Recommendation, in order: box growth, then the corner inset, then arrows ending on package tabs, then the label hybrid. Moving state names aside is small but worth only 4 bends. Details are in Ranking.
Per lever and diagram kind
Crossings, overlaps and bends, summed over each kind (65 component, 16 package and 22 state goldens).
| Variant | Component | Package | State | Total |
|---|---|---|---|---|
| Baseline | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 341 |
| Labels free: no waypoint, no label obstacles | 5 / 0 / 148 | 0 / 0 / 34 | 3 / 0 / 100 | 8 / 0 / 282 |
| Labels, no reservation in the layout | 5 / 0 / 148 | 0 / 0 / 34 | 8 / 0 / 90 | 13 / 0 / 272 |
| Labels hybrid, raw | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 1 / 148 | 5 / 1 / 330 |
| Labels hybrid, never worse per diagram | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 133 | 5 / 0 / 315 |
| Names ignored as obstacles only | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 341 |
| Names and tabs ignored everywhere | 5 / 0 / 146 | 0 / 0 / 28 | 0 / 0 / 155 | 5 / 0 / 329 |
| Titles realistic: tab ends, state names aside | 5 / 0 / 148 | 0 / 0 / 28 | 0 / 0 / 155 | 5 / 0 / 331 |
| Exact state header obstacles | 5 / 0 / 148 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 341 |
| Slides: corner inset 8, ends 8 apart | 4 / 0 / 136 | 0 / 0 / 34 | 0 / 0 / 149 | 4 / 0 / 319 |
| Slides: ends 8 apart only | 4 / 0 / 140 | 0 / 0 / 34 | 0 / 0 / 155 | 4 / 0 / 329 |
| Slides: corner inset 8 (12 on rounded states) | 5 / 0 / 136 | 0 / 0 / 34 | 0 / 0 / 159 | 5 / 0 / 329 |
| Box growth N = 1 | 5 / 0 / 102 | 0 / 0 / 32 | 0 / 0 / 151 | 5 / 0 / 285 |
| Box growth N = 2 | 5 / 0 / 94 | 0 / 0 / 32 | 0 / 0 / 125 | 5 / 0 / 251 |
| Box growth N = 4 | 5 / 0 / 82 | 0 / 0 / 20 | 0 / 0 / 111 | 5 / 0 / 213 |
| Titles + corner inset + growth N = 2 | 3 / 0 / 90 | 0 / 0 / 26 | 0 / 0 / 119 | 3 / 0 / 235 |
| Titles + corner inset + growth N = 4 | 3 / 0 / 78 | 0 / 0 / 20 | 0 / 0 / 109 | 3 / 0 / 207 |
| All realistic, labels included | 3 / 0 / 78 | 0 / 0 / 20 | 0 / 0 / 92 | 3 / 0 / 190 |
| All realistic, never worse per diagram | 3 / 0 / 78 | 0 / 0 / 20 | 0 / 0 / 87 | 3 / 0 / 185 |
Labels
Component and package diagrams already route first and place labels afterwards (labels::Placer),
so labels never cause a bend there: every label lever leaves them unchanged. In state diagrams a label is
a dummy in the layered layout (layered::EdgeLabel), the line is routed in two halves through the
label’s anchor, and every label is an obstacle for every other line.
Upper bound. Keeping the layout, which still reserves the room, but routing with no waypoint and no
label obstacles saves 59 of 159 state bends, and adds 3 crossings. Freed back edges bundle into one
corridor and cross (TCP connection: the rcv ACK and timeout 2MSL returns). Dropping the reservation
as well saves 69 bends, makes the state diagrams 20% smaller, and adds 8 crossings.
Realistic variant: route, then place. After free routing, each label was placed beside its own line. The candidates were every segment, at five positions along it, on both sides, 8 or 20 px off the line. The score counted hits on boxes, headers, other labels and lines, and preferred long segments, the middle, the flowing side and the near offset. Of the 109 state transition labels:
| Placement | Collide with something | Far from their line (over 12 px) | Ambiguous (another line at least as near) |
|---|---|---|---|
| Baseline, reserved spots | 0 | 0 | 3 |
| Free routing, placed after | 9 | 0 | 22 |
| No reservation, placed by the existing placer | 16 | 0 | 11 |
So one label in twelve collides and one in five sits as close to a foreign line as to its own. The
hybrid pins every label that collides, sits far or reads ambiguously, and every line that gains a
crossing. A pinned label gets its waypoint and obstacle back, and the routing repeats until stable.
It pinned 32 of 109 labels and saved 11 bends. It is worse than the baseline in three diagrams
(language-labels and stress-right-regions gain bends, one diagram gains an overlap). Keeping the
baseline in those raises the saving to 26 bends with no new crossing or overlap.
Where the bends come from: most are short transitions between neighbouring ranks, whose label dummy sits
at a cross position chosen for label room, not for the line. In Elevator controller, near target floor,
speed zero, levelled at floor and fully open each jog only to pass their anchor.
Titles
Composite names (component, state) and package tabs are routing obstacles, and they also narrow the
span where ends may attach. A package’s top side starts right of its tab. A child’s top side under its
parent’s tab starts clear of it (tab_clear, under_tab). A package used as a component container
takes ends only on the right half of its top. An entry point slides along the border off the state’s
centred name (clear_of_name).
- As obstacles alone, names cost nothing. Ignoring every name and header obstacle changed no line in any golden file. A more exact state header obstacle, with the name centred and the activities where they are drawn, also changed nothing.
- Through attachment spans, they cost 12 bends (component 2, package 6, state 4). Ignoring them
outright is not drawable: arrows then pierce package tabs and the entry line runs through
Login. - Realistic. Package arrows that arrive from above may end on top of the tab (UML 12.2.4 makes the
tab part of the package symbol): −6 bends in
language-relationsandlanguage-cycle. A composite state’s name moves left or right when an entry point wants the middle: 2 of the 11 composite state names moved, −4 bends. Moving component composite names helped nowhere, and 9.2.4.1 asks for them centred. Across 25 titled composites and 20 titled packages, a name moved twice.
Box size
For each edge with bends, one end box grew by 1 to N grid steps (8 px) on the side toward the other end, on either axis. The smallest growth that reduced that edge’s bends was kept, when the diagram’s totals got no worse and no two connectors merged. Growth was allowed only for plain leaf boxes, kept 16 px from every other box, stayed inside the parent’s padding and header, and stayed out of reserved labels, self-transition loops and interface stubs. The layout itself did not change, so the diagram’s bounds are unchanged.
| N | Bends saved | Boxes grown | Area added | Share of box area |
|---|---|---|---|---|
| 1 | 56 | 20 | 27,520 px² | 0.9% |
| 2 | 90 | 34 | 50,816 px² | 1.6% |
| 4 | 128 | 51 | 97,408 px² | 3.1% |
By kind at N = 4: component −66 (2.5% of box area), package −14 (3.0%), state −48 (5.5%). The images
read better: Event-driven checkout loses all four jogs once Message Bus, Payments and Inventory
gain 8–24 px. The cost is unevenness: boxes in one rank no longer share a height, and a box can grow on
one side only, so its name is off the line of its neighbours’ names. At N = 2 this is hard to notice.
Attachment along the side. Stage 7 already slides ends between their neighbours. What remains is
the 16 px corner inset and the 16 px gap between ends. An 8 px inset and an 8 px gap save 22 bends and
one crossing, but two ends then reach the same point. Hexagonal service’s “fixed” crossing is two
connectors leaving Use Cases together and splitting, which the metric excuses because they share an
end. The gap alone causes this, and the inset alone does not: an 8 px inset on square boxes saves 12
component bends with no merged end. On rounded state boxes (corner radius 10) an 8 px inset puts the
arrowhead on the curve. At 12 px it saves nothing.
Every bend by cause
Each of the 341 bends counted once, by its first cause in this order: label, title, attachment, box size, crowding by other lines, then geometry. A cause is credited only with bends its own counterfactual removed for that edge. Crowding was tested by routing each bent edge alone in the same geometry. The geometric tests take the two end boxes: 0 bends are needed if their attach ranges overlap across the gap, 1 if they are apart on both axes, and 2 otherwise. Bends beyond that minimum are a side choice when a clear L exists, or in the way when another box or label blocks the straight corridor. Bends within the minimum are necessary when the end shares its side with an edge to another node (a fan), and otherwise placement.
| Cause | Component | Package | State | Total |
|---|---|---|---|---|
| Label (reservation, waypoint, obstacle) | 0 | 0 | 62 | 62 |
| Title (spans narrowed by tabs and names) | 2 | 6 | 4 | 12 |
| Attachment: corner inset and gap | 12 | 0 | 2 | 14 |
| Attachment: port policy (inner straight, outer jogs) | 32 | 0 | 0 | 32 |
| Box size | 58 | 8 | 15 | 81 |
| Crowding by other lines | 0 | 0 | 0 | 0 |
| Side choice: a Z where a clear L fits | 11 | 6 | 11 | 28 |
| Rank or ordering: a box or label in the way | 12 | 2 | 22 | 36 |
| Rank or ordering: ends misaligned with no competing edge | 0 | 1 | 9 | 10 |
| Necessary: fan-in or fan-out on one side | 17 | 11 | 28 | 56 |
| Defect or limit | 4 | 0 | 6 | 10 |
| Total | 148 | 34 | 159 | 341 |
Box growth alone removes 128 bends, but only 81 are credited to it, because labels and attachment come first in the order. No bend is caused by other lines: routing each bent edge alone gave the same bends, so the router’s sharing and crossing penalties are not what bends lines.
The 10 defect bends are two patterns. In component diagrams (Compiler pipeline twice), the typed AST
line jogs between containers: each container is laid out on its own, so Type Checker and IR Builder
do not line up across them. This is the recursive arrange limit noted in the aesthetics report. In
state diagrams, a reverse pair between a state and a composite’s border (Off and Playing, Idle and
Operating) has 28 px of common span, too little for two ends at the 24 px slot spacing, so both lines
jog.
The five crossings
| Diagram | Lines | Cause |
|---|---|---|
| component/introduction-01 (Order Platform) | Billing to Event Bus crosses the SQL dependency from Order Service to Postgres | Rank or ordering. Event Bus sits above Postgres inside Infrastructure, but its clients lie above and below the SQL line. The container orders its children without seeing outside lines (recursive layout). |
| component/example-orders (Order Platform) | the same two lines | The same diagram as above. |
| component/example-simple (Compiler Pipeline) | invoke from Driver to Code Generator crosses SSA from Optimizer | Attachment order. The slot of invoke on Code Generator is left of SSA’s, because Driver is to the left, but the route has to come down the right of Front End. Slots are ordered by the peer’s position before routing. |
| component/gallery-tools-01 (Compiler pipeline) | the same two lines | The same diagram as above. |
| component/gallery-services-01 (Hexagonal service) | Use Cases to the repo port crosses Use Cases to Domain Model | Attachment order on Use Cases’ right side, made necessary by ranking. Domain Model sits between Use Cases and Core’s right-hand ports, so the delegation has to climb over it, yet its slot is below the other line’s. |
Box growth together with the corner inset removes the two Compiler pipeline crossings: Driver grows
8 px and invoke then leaves on the right. Nothing realistic removed the other three. Fixing them
needs ordering across containers (Forster, Sander) or slot order taken from the route.
Ranking
Bends and crossings saved, weighed against effort and risk.
- Grow end boxes to straighten connectors (−90 bends at N = 2, −128 at N = 4, no crossings, +1.6% or +3.1% box area).
Sketch: a pure stage in
coreafter layout and slot settling. For each edge whose two end ranges do not overlap, and whose corridor is clear, it computes the growth that makes them overlap by at least one slot. It grows a leaf when the room around it allows (16 px clearance, parent padding, keep-out rectangles from the adapter), then routes once. A second round catches edges that one growth unblocked. Adapters pass which nodes may grow and their keep-out areas, as they pass obstacles now. Rendering needs nothing, since names centre in the final rect. Effort: 2–3 days with tests and re-blessed goldens. Risk: uneven sizes in a rank. Start at N = 2, and consider growing all boxes of a rank together. - Corner inset of 8 px on square boxes (−12 bends). Sketch: pass 8 instead of 16 to the slide ranges of component and package boxes, and keep 16 on rounded state boxes and the 16 px gap between ends. Effort: hours. Risk: low. Do not shrink the gap: that merges ends.
- Arrows may end on a package tab (−6 bends). Sketch: in package routing, a top slot inside the tab’s span gets the tab’s top edge as its point, and the top side’s span starts at the left corner. Effort: half a day. Risk: low. UML 12.2.4 draws the tab as part of the symbol.
- Label hybrid in state diagrams (−26 bends). Sketch: route without waypoints, place labels beside their own lines with a quality check (no collision, not far, not ambiguous), pin the failures back to their reserved spot and re-route, and keep the reserved drawing for a diagram where this is worse. This needs a label-quality measure in the quality test first, so that a regression shows. Effort: 3–5 days. Risk: medium to high. Ambiguous labels are worse than a jog (see the principles), and freed back edges cross.
- State names give way to entry points (−4 bends). Sketch: when an entry point wants the middle of a composite’s top, put the name left (or in a UML name tab, 14.2.4.4) instead of sliding the entry point off it. Effort: a day. Risk: medium, because names in one diagram are then aligned differently.
Not recommended: dropping label reservation (+8 crossings, 16 collisions); shrinking the gap between ends (merged connectors); moving component composite names (no gain, and 9.2.4.1 asks for them centred).
Expected totals. Levers 1 to 3 and 5 at N = 4: 3 crossings, 0 overlaps, 207 bends, down from 5/0/341 (−39% bends), measured together. At N = 2: 3/0/235. Adding the label hybrid: 3/0/185.
Further levers found on the way, not measured as realistic variants: choosing an L over a Z when the L is clear (28 bends), letting an outer connector take the straight line at a port when the delegation inside has room (32 bends), slot order taken from the route’s direction of arrival (3 of 5 crossings), and crossing reduction across containers (2 crossings, 4 bends).
Images checked
Rasterized with resvg at 1.5× and inspected:
- Labels: Elevator controller and TCP connection, baseline, free placement and hybrid. Free
placement straightens
Moving→Stopping→LevelledandClosed→Opening→Open, but putsnear target flooron thenext callline and lets the returns toIdleandClosedcross. The hybrid keeps the straight lines, but in TCPrcv ACKfloats between two parallel returns. - Titles: Relations (package), baseline, names ignored and tab ends. Ignoring names lets arrows
pierce the tabs. Ending on the tab reads correctly. Entry and exit points: ignoring the name runs the
entry line through
Login, which is why the realistic variant moves the name instead. - Box size: Event-driven checkout and Connectors (component), Elevator controller (all levers). Every saved bend reads better. Uneven heights show at N = 4 in Connectors.
- Attachment: Long labels (state) at an 8 px inset puts the arrowhead into
B’s rounded corner. Hexagonal service at an 8 px gap merges two connectors. - Crossings: Order Platform, Compiler Pipeline and Hexagonal service, baseline and all levers.
Method notes
- Metrics are those of
core::metricsover the router’s polylines. A merged pair of ends, two different connectors leaving one point along one line, was counted separately, becauseoverlapsexcuses lines that share an end. The baseline has two merged pairs, both intentional fan-ins to one port. - Growth was greedy in edge order and verified by re-routing the whole diagram per trial. A real implementation should predict growth geometrically and route once or twice.
- The label placer of the realistic variant was written for the experiment. The existing
Placerallows only single-line labels and five positions. Its collision and ambiguity counts are therefore a fair estimate, not a lower bound.
How layout works
drumlin encodes the habits of a careful draftsman as rules.
Component, package and state diagrams share the layered layout and router described below. Sequence diagrams use their own timeline layout: steps resolve execution depth, columns are spaced from labels and frames, and rows follow the page. Activity diagrams use a structured-flow layout driven by the block tree, one measure stage and one place stage.
Steps
Each step enforces some of the drawing principles. The principles say what a drawing should look like; the steps below are how drumlin gets there.
Compound layout
Each container is laid out bottom-up as a layered graph of its direct children. An inner layout becomes one large item in its parent. See Group what belongs together.
Ranking
Connectors and hints assign each item a rank. Long edges get invisible dummy items that hold a lane through the ranks between. See Flow one way.
Crossing reduction
Weighted median sweeps reorder each rank, alternating down and up, and a transpose pass swaps neighbours while that removes crossings. The sweeps run twice, from the input order and from a depth-first order, so a tree starts without crossings, and the best ordering seen wins. Hints and notes are applied inside every sweep, so the ordering is scored as it will be drawn. See Order ranks to avoid crossings.
Placement
An order-preserving median solve aligns connected items and snaps everything to an 8px grid. The dummies of each long edge then line up where both ends can take the line, so it runs straight, or where one end can, so it bends only once. Edges between the same two items move as a block. Pendant items, such as an initial state above its first state or a final state below its last, move in line with what they hang from. State transition labels get a reserved place in the gap between ranks. See Keep long edges straight and Reserve room for labels.
Box growth
Where a connector jogs because its end boxes miss each other by a step or two, core::grow grows
one end box by up to 16px on a side toward the other end, into free space, and routes the diagram
again. A growth is kept only when that connector loses a bend, no crossing, overlap or bend total
rises, and no two connectors come to leave one point together. Growth across the flow is tried
first, and only growth that lets the two boxes share a span is routed. See
Grow a box rather than bend a line.
Slots
Attachment points slide along box sides toward their peers. This is what makes most connectors straight. They are spread 16px from corners, and nudging may slide an end to 8px from a square corner. A package without visible contents also takes ends on top of its blank tab. See Spread and order attachment points, Keep ends off corners and Let arrows end on a package’s tab.
Routing
A* runs over a sparse grid with costs for length, bends, crossings, shared lanes and travel along container boundaries. Three rip-up rounds settle conflicts. See Draw lines orthogonally and Avoid crossings.
Nudging
Each segment then moves along its own axis to where its line overlaps, crosses, crowds and bends least, in that order, and never through a box, a reserved label or a container boundary. Two lines tangled in one channel move together. Jogs fold away where a straighter path is clear, and an end may slide along its side, between its neighbours, to straighten its line. Finally, the middle runs of Z-shaped lines that share a channel spread evenly across it, and a lone one sits in the middle of its gap. See Never lay lines on top of each other and Space parallel lines evenly.
L over Z
After nudging, a line shaped like a Z, whose two end runs are parallel, becomes an L when one of its
ends may move to the side of its box that faces the line: the first run continues past the box’s
corner and turns into that side. Adapters say which ends may move by giving a terminal its box
(router::Body). The L is kept only when it meets no obstacle or strip more than the Z did, its new end
keeps SLIDE_GAP from other ends on that side and 16px from the corners, it gains no overlap,
crossing or close run, and no two lines come to leave one point together. Among the Ls that pass, the
best standing wins, then the end nearest the middle of its side. Because it is part of router::route,
box growth and the label hybrid see the L. See
Prefer an L to a Z when the L is clear.
Labels after routing
State diagrams (diagrams/state/hybrid.rs): after box growth, the diagram is routed with every label
reserved. Each labelled line that still bends is then freed: routed without its label’s waypoint, with
its label no longer an obstacle. Each freed label is placed beside its new line: five positions along
every segment, on both sides, ink 8px off the line, on a segment that reaches 8px past the label. A
place must touch no line, arrowhead, shape or label (4px air, 8px from other labels, 6px from outlines
and region dividers), stay inside the diagram, sit within 12px of its own line, keep the rest of its own
line as far, and keep every other line 12px further. The best place prefers the flowing side (right of
a vertical run when the flow is down, above a horizontal run when it is right), the middle of long runs,
and room from other lines. A label with no place, or whose line gains no straightness or gains a
crossing, is pinned back to its spot and the rest are routed again, up to four rounds. The result
replaces the reserved drawing only when it has fewer bends and no more crossings, overlaps, merged
ends, failed routes or label flaws (metrics::read_labels). Each round costs one route.
Labels
Component and package labels, and state labels with no reserved room, take the position with the fewest collisions. See Labels.
Measuring quality
The quality test in src/diagrams/quality.rs counts crossings, overlapping runs,
bends and merged ends (two connectors leaving one point together) over every
component, package and state golden file, and fails if a change raises a total. Run cargo test --lib quality -- --nocapture for the table.
The same test reads every connector label in those files (core::metrics::read_labels): labels within
2px of a line, outline or arrowhead, labels on shapes or other labels, labels with another line at least
as near as their own (ambiguous), labels more than 12px from their own line, and the summed distance
from each label to its own line. Each kind builds this as a Scene with owners
(src/diagrams/connectors.rs). Interface names on assembly connectors read with their ball and count
only as text to keep clear of. Set DRUMLIN_LABELS=state/<name> and run the explain_labels test to
list the flaws in one golden file.
Sequence and activity diagrams have no router, so the same test measures their whole drawing instead.
Each kind describes its output as a Scene (src/diagrams/<kind>/scene.rs): connectors, opaque shapes,
shapes drawn under them, strokes text must avoid, free text, contained text and gaps meant to be equal.
core::metrics::assess then counts, per kind, only what layout decides:
| Count | Sequence | Activity |
|---|---|---|
| Crossings and overlapping runs | between messages | between flows and note links |
| Bends | not counted: a self message always has two | every flow |
| Lines through shapes | messages through heads, notes, refs, tabs | flows through nodes and notes |
| Shape overlaps | tabs or notes over execution bars, shapes on shapes, shapes across frame edges | nodes and notes on each other or across partition dividers (fork bars may span) |
| Labels on lines | message labels and guards within 2px of a message, lifeline, frame edge, arrowhead or dot | guards within 2px of a flow, link, divider or arrowhead |
| Labels on shapes and labels | against heads, bars, notes, refs, tabs | against nodes, notes, lane heads |
| Text overflow | names in heads, note and ref text, operators in tabs | names in nodes, note text, lane names |
| Uneven gaps | rows of consecutive one-line messages with nothing between | straight drops from node to node, except into a merge or bar |
Messages crossing lifelines and bars are the notation, so they are not counted. Diamonds and circles
count as the largest square inside them, because flows end on their outline. A gap is uneven when it
differs from the most common gap by more than half a grid step. Set DRUMLIN_SCENE=activity/<name> and
run the explain_scene test to list what was counted in one golden file.
Style rules
These are stated with their reasons in Shapes and spacing.
- Every box and line coordinate sits on an 8px grid.
- Lines are orthogonal with small rounded corners.
- All shapes use one line weight, one ink colour and a white fill.
- Text widths come from embedded Helvetica metrics, so layout needs no font files and is identical on every platform.
The measured effect of further changes (label freedom, names giving way, larger box growth) is in Layout levers. Box growth, the 8px corner inset and tab ends are in place since stage 8; L over Z and state labels after routing since stage 10.
Rendering at docs build time
No diagram on this site is a checked-in image. Authors write a fenced code block, and a preprocessor replaces it with inline SVG while the site builds.
How to write one
Use a code fence with the language drumlin:
```drumlin
component Client { requires IGreeting }
component Server { provides IGreeting }
```
The page then shows the source and this diagram:
Fence options
| Fence | Output |
|---|---|
```drumlin | Source, then diagram |
```drumlin,render-only | Diagram alone |
```drumlin,error | Source, then drumlin’s real error message |
Set up
The preprocessor is a small Rust program that links drumlin as a library.
# book.toml
[preprocessor.drumlin]
command = "mdbook-drumlin"
cargo install --path docs/mdbook-drumlin
mdbook build docs
Why render during the build
- Examples cannot go stale. Each image always comes from the source beside it.
- A broken diagram breaks the build. A parse error stops
mdbook build. So does adrumlin,errorexample that parses successfully. - Error messages stay honest. The Errors page shows output captured at build time.
- Pages need no client-side rendering. Readers get plain SVG with no JavaScript.
- Deployment is a Docker build. The
Dockerfilecompiles drumlin, builds the book and copies the static result into nginx.
Roadmap
| Area | State |
|---|---|
| Component diagrams, with interfaces, ports and delegation | Prototype |
| Packages as containers | Prototype |
Package diagrams with «import», «merge» and containment | Prototype |
| Sequence diagrams with fragments, activations, creation and destruction | Prototype |
| State diagrams with composite states, regions and pseudostates | Prototype |
| Activity diagrams with decisions, loops, forks, signals, objects and partitions | Prototype |
| Notation audit against UML 2.5.1 | Done for all five kinds |
| JSON diagnostics | Under consideration |
| Pluggable layout engines, and layout research items | Not scheduled |
Known limitations
All kinds:
- No diagram has a UML frame, such as
sdorpkg. UML makes the frame optional, and a title is a caption outside the drawing. - Builds have been tested only on Linux so far.
Component, package and state diagrams:
- Coordinates come from median placement, not an optimal solve, so an item can sit off the line of what it connects to, and the connector then bends. Reserved labels can force a long edge to bend around them.
Component diagrams:
- Interface inheritance (
^), port type labels, several interfaces on one lollipop and the optional compartments of a component are not expressible. All are optional in UML. - Connectors sometimes carry a small jog near a box corner. A route can go around a composite’s name.
Package diagrams:
- Classifiers have no bodies, so attributes and operations cannot be listed.
- A dependency arrow between packages that do not overlap across the flow carries one jog.
Sequence diagrams:
- Gates and coregions are optional UML notation and are not drawn; found and lost messages and
parcover the need. - Lifeline heads are not repeated at the bottom.
- A fragment’s operator tab can hide part of an active execution bar on the first lifeline it covers, and a note across several lifelines hides the bars under it.
State diagrams:
- Long edges in dense machines may still bend around other transitions’ labels.
- Labels of several self-transitions on one side are stacked and offset, which makes them harder to match to their loops.
- Local transitions are drawn on the right side of a composite.
Activity diagrams:
- Flows are block-structured, so unstructured flows cannot be written.
- Interruptible regions, pins, exception handlers, activity parameter nodes and hierarchical partitions are not drawn.
- The next statement after a side arm that stops still waits below that arm, because rows are shared.
- A note above a decision lengthens the flow into it, so a straight flow is not evenly spaced there.
- When branches stack because one crosses lanes another occupies, a flow can cross the stacked branch (Branches that interleave lanes).