Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.
Order Platform Edge Web Gateway Order Service API Handler Pricing Engine Outbox Publishes at least once. Consumers deduplicate on event id. «external» Identity Infrastructure Event Bus «database» Postgres Billing IInvoices IEventBus IEventBus IIdentity IOrders IPricing api events persist SQL

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 }
Hello Client Server 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

CommandEffect
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 }
Hello, again Backend Server «database» Store Client IGreeting read

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 right A 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
direction down A 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 check reports a position and a likely fix.

A typical loop

Agent feedback loop Agent drumlin check drumlin render «human» Reviewer IDiagnostics IModel SVG

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
}
Parts and grouping Platform «service» Request Router Parser Dispatcher «database» Orders 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
Connectors Producer Consumer Metrics batches 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:

EndsWhat is drawn
Two simple ports (one interface each)Ball-and-socket: the ball seated in the socket
A port with several interfaces at either endA 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 }
Matched by name, no ports Client Server IHealth IOrders

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
}
Ports Gateway Bus Queue IEvents uplink in

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.
Notes and hints Cache Store Entries expire after 30 seconds. writes through

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
Kernel subsystems System Call Interface Core Process Scheduler Memory Manager I/O VFS Network Stack Block Layer Device Drivers page faults

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.
LSM storage engine Client Engine Write Path Read Path Memtable Write-Ahead Log Compactor Replayed on startup before the engine accepts writes. Block Cache «filesystem» SSTable Files ICache IKeyValue kv append insert probe uses flush, read

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
Type 1 hypervisor Guests Linux Guest RTOS Guest Virtual Devices Hypervisor vCPU Scheduler Stage-2 MMU Hardware Drivers IDriver IVirtio IVirtio 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.
Container runtime CLI Daemon Image Store Shim «platform» Linux Kernel Outlives the daemon so that containers survive restarts. IImages IOci IRuntimeApi ISyscalls

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.
Motor controller firmware Application Speed Loop Fault Monitor Drivers PWM ADC CAN Control Task Telemetry Runs at 20 kHz in interrupt context. IAdc ICan IPwm limits setpoint

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
Battery sensor node Sampling Task I2C Driver Radio Task LoRa Driver Power Manager II2c IRadio ISamples wake 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
RTOS layers Middleware File System TCP/IP Kernel Tasks Queues Timers «port layer» 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.
Secure boot chain «immutable» ROM First Stage Verifier Second Stage Kernel Image Holds the root public key hash. IVerify verifies loads verifies, starts

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 }
Vehicle gateway Powertrain ECU Body ECU Gateway Router Firewall Telematics Unit IEthernet IFrames IFrames can can bus uplink

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
Compiler pipeline Driver Front End Lexer Parser Type Checker Back End IR Builder Optimizer Code Generator ITarget ITokens tokens AST SSA typed AST 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
Language server Editor Server Dispatcher Incremental Parser Symbol Index Diagnostics File Watcher ILsp lsp didChange update errors 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.
Build system «input» Build Files Graph Builder Scheduler Local Executor Content Cache Keyed by a hash of each action's inputs. ICache IExecutor parse actions

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 }
Debugger Debug UI Engine DWARF Reader ptrace Backend IDebug ISymbols 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
Documentation build «input» Markdown Chapters mdBook mdbook-drumlin drumlin library «output» Static Site IPreprocessor IRender 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 }
Hexagonal service HTTP Adapter Core Use Cases Domain Model SQL Adapter Kafka Adapter IEvents IRepository IUseCases api repo publish

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 }
Replicated state machine Client Replica Consensus Module Log State Machine Peer Replicas IPropose IRaftRpc propose peers append apply committed

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
Cache-aside read Service «cache» Redis «database» Postgres 1. get 2. on miss 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 }
}
API gateway Mobile App Web App Gateway Services Auth Catalog Checkout IAuth ICatalog ICheckout IPublicApi IPublicApi

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.
Event-driven checkout Checkout Message Bus Payments Inventory Shipping Delivery is at least once. Handlers are idempotent. IBus OrderPlaced OrderPlaced PaymentCaptured StockReserved

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
Nightly ETL «database» Source Databases Extract Transform Load «database» Warehouse Data Quality parquet 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
Stream processor Producers Partitioned Log Job Windowing Aggregator Sink «rocksdb» State Backend ILog ISubscribe source 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
Query engine SQL Parser Planner Catalog Optimizer Executor Storage Layer ICatalog AST logical plan physical plan 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
Model serving Client Serving Router Model A Model B «database» Feature Store Metrics Sink IPredict 95% 5% lookup 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
}
Names in the tab, or in the body Empty Types Integer 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
Nesting and keywords «model» Platform Kernel Scheduler Timer Drivers Empty «access»

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

ArrowMeaningDrawn as
a ..> bDependencyDashed line, open head
a --> bDirected associationSolid line, open head
a -- b, a .. bPlain lineSolid or dashed, no head
a --|> bGeneralizationSolid line, hollow triangle
a ..|> bRealizationDashed line, hollow triangle
a +-- bContainmentSolid 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
Relations App Lib Util Base «import» «access» «merge» 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
Classifiers «interface» Shape Circle Square Base «enumeration» 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
Containment Types Shape Point 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>>
Containment, left to right api core Router Handler «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>>
}
Nested import Core Scheduler «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.
Notes and hints Core Parser Checker Emitter Runtime Stdlib Tool Built separately. «import»

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
}
Unconnected members fold into rows Kernel Scheduler Timer Mutex Channel Pipe 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.
Layered architecture Presentation Web Mobile Application Commands Queries Domain Order Customer «interface» Repository «enumeration» Status Infrastructure Persistence Messaging Depends on nothing else. Keep it free of framework types. «import» «import» «access»

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
Library dependency graph app http-client json tls logging platform «import» «import» «import» «access»

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>>
Package merge P A B Q C R D S T «merge» «merge» «merge» «merge» «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.
Plugin system Host Registry «interface» Plugin «framework» plugins Markdown MarkdownPlugin Search SearchPlugin Discovered at startup from the plugin directory. loads

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>>
Domain model Ordering Order LineItem «enumeration» OrderState «interface» Pricing Catalog Product Category contains «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
}
Ordering a pizza Customer Web app Kitchen place(order) cook(order) 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
Heads Operator «service» Controller Disk mount() spin up

Messages

Arrows select the kind of message, following UML 2.5.1 clause 17.4.4.

ArrowMessageLine and head
a -> bsynchronous callsolid, filled head
a ->> basynchronous messagesolid, open head
a --> breplydashed, 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
Kinds of message Client Server request() response fire and forget log the request 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
}
Nested calls main() Parser Lexer parse(source) next() skip whitespace token build tree 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
Worker lifetime Pool Worker spawn() run(job) result stop

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
}
Boundaries Bus Handler interrupt dispatch() retransmit (no ack) 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
  }
}
Retries and alternatives opt [worth caching] alt [cached] [miss] loop(1,3) Client Server Cache fetch(key) get(key) value recompute put(key, value) 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()
}
Parallel and critical regions par critical [mutex held] Task A Lock Task B acquire() acquire() granted update state 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.
Boot Bootloader Kernel Init PID 1 ref Verify signature Interrupts stay masked until the scheduler starts. Everything runs under this process. jump(entry) exec

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.
TCP connection opt [segment lost] Client Server Three-way handshake Established Graceful close TIME_WAIT for 2 MSL before the socket is freed. SYN seq=x SYN+ACK seq=y, ack=x+1 ACK ack=y+1 data seq=x+1, len=512 ACK ack=x+513 data seq=x+513 retransmit after RTO FIN ACK FIN ACK

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
}
Raft: replicating one log entry alt [log matches prevLogIndex] [prevLogTerm mismatch] par critical [majority acknowledged] Client «raft» Leader Follower 1 Follower 2 State machine Leader walks nextIndex back and retries. command(x) append to log AppendEntries(term, entry) append and persist success AppendEntries(term, entry) append and persist success reject (conflict index) advance commitIndex apply(x) result 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
}
Nested interrupt handling opt [reschedule pending] Timer CPU core Kernel entry UART ISR Timer ISR Scheduler Higher priority: the ISR is preempted. UART IRQ vector through IDT save registers uart_isr() read FIFO timer IRQ timer_isr() mark reschedule eoi eoi check need_resched schedule() next task iret 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
}
TLS 1.3 handshake alt [certificate valid] [verification failed] Client Server CA store Encrypted from here on: EncryptedExtensions, Certificate, CertificateVerify, Finished Application data, protected by traffic keys ClientHello(key_share, sni, supported_versions) derive handshake secrets ServerHello(key_share) EncryptedExtensions Certificate CertificateVerify verify chain(certificate) ok Finished Finished HTTP request HTTP response alert(bad_certificate) 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.
RPC with retry break [success] alt [reply before deadline] [deadline exceeded] loop(1,3) Client stub Circuit breaker Service The stub surfaces the last error once retries run out. allow? true GetOrder(id, deadline=200ms) order record success sleep(backoff with jitter) record failure surface error to caller

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
}
Page fault alt [anonymous page] [invalid access] MMU Kernel Swap device «per fault» Fault context The request is queued and the task sleeps. translate(vaddr) page fault(vaddr) allocate find VMA read(slot) page map page SIGSEGV to task 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]
Door Closed Closing Locked Open Opening open [unlocked] / start motor fully open / stop motor close fully closed obstruction / reverse motor lock 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
Transition labels Armed Firing Waiting arm, rearm [sensors ready] / log("armed") disarm trigger [armed and not locked] / ignite(); start_timer() done tick / count() drift [abs(x) > limit] / correct()

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
Password entry Typing entry / set echo invisible exit / set echo normal character / handle character help / display help 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
Composite states and history Playing entry / spin up do / stream exit / spin down H Seeking Track Off Paused power seek found power power next / advance pause resume

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
  }
}
Elevator controller Operating entry / release brake exit / engage brake motion Levelled Moving Stopping doors Closed Closing Open Opening Fault Idle call received / plan route queue empty technician reset near target floor speed zero next call levelled at floor fully open door timer fully closed obstruction motor overcurrent / cut power

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
Every pseudostate Tasks left A right B Queued Ready Worker request [workers free] / assign [all busy] worker free done / prepare fatal

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
Entry and exit points Login Check Prompt Done Idle again aborted retry submit [locked out] [ok]

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 --> [*]
Local transitions Active One Two next reset

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
TCP connection CloseWait Closed Established FinWait1 FinWait2 LastAck Listen SynRcvd SynSent TimeWait passive open / create TCB active open / send SYN rcv SYN / send SYN+ACK rcv SYN+ACK / send ACK rcv ACK close / send FIN rcv FIN / send ACK rcv ACK rcv FIN / send ACK rcv ACK timeout 2MSL close / send FIN

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
}
USB device Configured entry / enable endpoints exit / disable endpoints SOF / update frame counter Address Attached Default Powered Suspended hub configured bus reset set address set configuration [config != 0] set configuration [config == 0] bus idle 3 ms bus activity hub deconfigured bus reset

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.
Process lifecycle New Ready Running Waiting Zombie Holds only its exit status until the parent calls wait. admitted dispatch preempt [quantum expired] event occurs exit() / release resources reaped by parent syscall [fast path] / handle inline wait(event) / block()

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
  }
}
Elevator controller Operating entry / release brake exit / engage brake motion Levelled Moving Stopping doors Closed Closing Open Opening Fault Idle call received / plan route queue empty technician reset near target floor speed zero next call levelled at floor fully open door timer fully closed obstruction motor overcurrent / cut power

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 --> [*]
OTA firmware update Downloading entry / start watchdog exit / stop watchdog chunk received / append to flash Fetching Verifying Confirmed Idle Installing Rebooting RolledBack update available / open session last chunk [hash ok] [hash bad] / clear buffer network lost image written / mark pending boot complete [self test passed] [self test failed] / restore slot A

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
}
Circuit breaker Closed do / count failures HalfOpen Open failures >= threshold / start timer timer expired probe succeeded / reset failure count probe failed / restart timer success / reset failure count call / fail fast

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
Actions in a flow Validate request Handle Order Store order

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
Decisions Parse header Route request Log error Serve from cache Send 403 Run handler Cache Count Alert Mystery [valid] [malformed] [cached] [else] [rejected] [else] [2xx] [4xx] [5xx] [else]

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
Multi-way decision Read Create Replace 405 «decisionInput» HTTP method [GET] [POST] [PUT] [else]

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
Loops Pop task Run task Read block Verify CRC [queue not empty] [else] [CRC ok] [else]

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
Concurrent flows Fetch profile Fetch orders Fetch feed Compose page Audit Respond

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
Signals Request payment PaymentRequested PaymentConfirmed Ship goods

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
Swimlanes Customer Warehouse Place order Pick items Ship Choose substitute [in stock] [else]

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
Notes Load config Enable traces Run Falls back to defaults when the file is missing. Checked once at startup. [debug] [else]

Rules from the specification

drumlin rejects flows that UML forbids and stops problems early:

  • start must 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 stop or end.
  • Statements after stop or end are 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
Build and release pipeline Fetch sources Resolve dependencies Lockfile Compile Link Run unit tests Lint and audit Package artifacts Sign artifacts Publish release Notify the team Compile, test and lint run concurrently. [tag build] [else]

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
Interrupt service with deferred work IRQ line raised Acknowledge at the controller Read status register Copy bytes to the ring buffer Wake softirq Count overrun Return from interrupt softirq scheduled Pop one packet Deliver to the stack Drop and count [FIFO has data] [overrun] [ring buffer not empty] [else] [checksum valid] [else]

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
Connection retry with backoff Resolve host Open socket TLS handshake Wait for backoff Send greeting Report unreachable Report DNS failure Mark session ready [connected or attempts exhausted] [else] [failed] [else] [connected] [else] [resolved] [else]

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
Deployment with parallel checks Developer CI Staging Security Production Merge to main Build image Image ready Image ready Deploy to staging Smoke tests Scan image Contract tests Roll out canary Roll back and file issue [all green] [else]

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
Firmware boot Clock and RAM init Record fault Verify slot A Verify slot B Enter recovery Jump to application Enter recovery «decisionInput» boot slot The vector table is relocated first. [reset cause is brown-out] [else] [A] [B] [else] [signature valid] [else]

Elements of component diagrams

ElementSyntax
Componentcomponent "Name" as a <<stereotype>> { ... }
Interfaceprovides I, requires I
Portport p left provides I
Connectora --> b : label
Packagepackage "Name" { ... } around components
Notenote 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.
All the component elements Client Backend Service Worker «database» Database Retries on timeout. IApi api queries

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
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
Memory Allocator Page Table

Stereotypes

A stereotype goes in << and >>.

component "Orders DB" as db <<database>>
component "Billing API" as api <<external>>
api --> db
«database» Orders DB «external» Billing API

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
}
Network Stack Socket 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
}
Kernel I/O VFS Block Memory

One-line blocks

A short body can share the line with its braces.

component Sensor { provides ISample }
component Logger { requires ISample }
Sensor Logger 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 }
Consumer Broker IQueue

Between two simple ports:

component Consumer { port in requires IQueue }
component Broker { port out provides IQueue }
Consumer Broker IQueue in out

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
}
Codec IAllocator IDecode IEncode

Several interfaces

Separate names with commas.

component Runtime { provides ITimer, IThreads, IFiles }
component App { requires ITimer, IThreads, IFiles }
Runtime App IFiles IThreads ITimer

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 }
Logger Net Disk UI ILog ILog ILog

Inside composites

Matching also works inside a composite and across nesting levels.

component App {
  component Controller { requires IRepo }
  component Repo { provides IRepo }
}
App Controller Repo 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
}
Modem serial 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 }
Radio Stack IPackets rx input

An unconnected port interface draws as a stub on the port.

component Controller {
  port ctl provides IControl
  port bus requires ICan
}
Controller ICan IControl ctl bus

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
}
Board power ground uart spi

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 }
Client Service Router Worker Database IApi IStore api store

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.

SyntaxMeaning
a --> bSolid line, open arrow at b
a ..> bDashed dependency, open arrow at b
a -- bSolid line, no arrow
a .. bDashed line, no arrow
a <-- bSame as b --> a
a <.. bSame as b ..> a
component A
component B
component C
component D
A --> B
A ..> C
B -- D
C .. D
A B C D

Labels

Text after : labels a connector.

component Producer
component Consumer
Producer --> Consumer : batches, 64 KiB max
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
Parser Lexer Preprocessor next token include 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

ElementSyntax
Packagepackage "Name" { ... }
Classifierclass, interface, enum
Relationsa ..> b, a --> b, a --|> b, a ..|> b
Import and mergea ..> b : <<import>>
Containmenta +-- b
Notenote 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.
All the package elements App Handler Core «interface» Store SqlStore Only depends on Core. «import»

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 }
Tab or body Empty Types Point

In a component diagram:

package "Domain" {
  component Orders
  component Customers
  Orders --> Customers
}
Domain 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
Application Handlers Domain Model Adapters SQL Kafka implements implements

Packages can nest.

package "Platform" {
  package "Storage" {
    component Blob
    component Index
    Index --> Blob
  }
  package "Compute" {
    component Runner
  }
  Runner --> Index
}
Platform Storage Blob Index Compute Runner

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>>
Classifiers Order «interface» Repository «enumeration» Status Shape

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

Domain model, Plugin system.

Relations

Relations between packages and classifiers in a package diagram.

ArrowRelationDrawn as
a ..> bDependencyDashed line, open head
a --> bDirected associationSolid line, open head
a -- bAssociationSolid line, no head
a .. bPlain dashed lineDashed line, no head
a --|> bGeneralizationSolid line, hollow triangle
a ..|> bRealizationDashed 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
Relations «interface» Printable Report Invoice Ledger writes 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

Plugin system, Domain model.

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>>
Import, access, merge App Lib Util Base «import» «access» «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>>
}
Nested import Core Scheduler «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
Containment Types Shape 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

ElementSyntax
Lifelineparticipant, actor
Messagea -> b, a ->> b, a --> b, [-> a, a ->]
Executiona -> b : call { ... }, return
Creation and destructiona -> new "B" as b, create, destroy
Combined fragmentalt, opt, loop, break, critical, neg, par
Interaction useref over a, b : Name
Notenote 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.
All the sequence elements opt [cached] User Server Worker Stateless. request() hit start result

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()
Heads Operator «service» Controller 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.

ArrowMessageLine and head
a -> bsynchronous callsolid, filled head
a ->> basynchronoussolid, open head
a --> breplydashed, open head
[-> afound (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
Message kinds Client Server wake call() reply notify 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
}
Nested executions App Parser parse() next token 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
Worker lifetime Pool Worker spawn() stop

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
  }
}
Alternatives loop(1,3) alt [ok] [retry] Client Server get 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)
Boot Bootloader Kernel ref Verify signature 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

ElementSyntax
Statestate Name { ... }, entry /, do /, exit /
Regionregion "name" { ... }
Pseudostates[*], choice, junction, fork, join, terminate, [H], [H*]
Entry and exit pointsentrypoint e, exitpoint x
Transitiona --> b : trigger [guard] / effect
Notenote a: text
diagram state "All the state elements"
[*] --> Idle
Idle --> Busy : start [ready] / log()
state Busy {
  entry / open()
  [*] --> Work
  Work --> [*]
}
Busy --> [*]
All the state elements Busy entry / open() Work Idle start [ready] / log()

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 --> [*]
Simple and composite Running entry / open() do / work() Step Idle start

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 --> [*]
  }
}
Regions Active input Reading output 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

Elevator controller.

Pseudostates

PseudostateSyntaxSymbol
Initial[*] --> xFilled circle
Final statex --> [*]Bull’s-eye
Choicechoice cDiamond
Junctionjunction jSmall filled circle
Forkfork fSolid bar, one in and at least two out
Joinjoin jSolid bar, at least two in and one out
Terminateterminate tCross
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 --> [*]
Choice, fork, join A B Done Ready request [ok] [denied]

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
Entry and exit points Login Prompt Idle again aborted retry [locked out]

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
Transitions Armed Waiting arm [ready] / log() done tick / count()

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

ElementSyntax
Actionaction "Name"
Initial and final nodesstart, stop, end
Decision and mergeif, switch, while, repeat
Fork and joinfork { ... } and { ... }
Object nodeobject "Name"
Signalssend "Name", accept "Name"
Partitionlane "Name" { ... }
Notenote: text
diagram activity "All the activity elements"
start
action "Receive"
if "valid" {
  action "Process"
} else {
  action "Reject"
}
stop
All the activity elements Receive Process Reject [valid] [else]

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
Actions Validate request Handle

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
Control nodes Audit Respond

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
Decision and loop Accept Reject Read [valid] [malformed] [more input] [else]

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
Fork and join Fetch profile Fetch orders Compose page

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
Object on the flow Resolve dependencies Lockfile Compile

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

Build and release pipeline.

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
Signals Request payment PaymentRequested PaymentConfirmed Ship goods

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
Swimlanes Customer Warehouse Place order Pick items

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.
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.
}
Storage Journal Pages 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.
Reader Writer Lock-free. May retry. Single owner only. wakes

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.
A note on a state 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, requires and port belong 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.

HintEffect
a left-of b, a right-of bHorizontal order
a above b, a below bVertical order
align a b cPlaces 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
Without align Gateway Auth Orders
diagram component "With align"
component Gateway
component Auth
component Orders
Gateway --> Auth
Auth --> Orders
align Auth Orders
With align Gateway 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
Metrics above Logs App Logs Metrics

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
Order without connectors Boot Init 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
Ingest Reader Serve Query Admin Console

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.

A careful drawing Idle Paused Running start pause resume stop

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:

  1. 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.
  2. 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.
  3. 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.
  4. No crossings. Crossings matter most in every human-factors study of graph drawing (Purchase 1997; 93% preference in Purchase et al. 2002).
  5. Few bends. Bends come next in the preference data (91%), and continuity of a path helps it be traced (Ware et al. 2002).
  6. Flow and alignment. A single direction, ranks, centred and symmetric placement.
  7. Compactness. Small area and short lines, but never at the cost of the rules above.
  8. 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

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
Lines straightened by a taller box Checkout Message Bus Payments Inventory OrderPlaced 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.

A B C

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.

A B C D

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.

A B

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
Labels beside their lines Busy Idle start [ready] / log() 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.

A B C D retry

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>>
Names in tabs Core Order Invoice 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.

Util App

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
One direction Parser Checker Emitter AST 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 --> [*]
Pendants in line Done Ready finish

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
Labels between lifelines User API DB place order insert(order) id 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
Main flow straight Receive order Ship Cancel Invoice [in stock] [no]

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).

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

LeverUpper boundRealistic variantCrossingsArea costEffortRisk
Box growth, up to 4 grid steps—−128 bends0+3.1% of box area, no growth of the diagramMedium (2–3 days)Low to medium: uneven box sizes
Box growth, up to 2 grid steps—−90 bends0+1.6% of box areaMediumLow
Box growth, 1 grid step—−56 bends0+0.9% of box areaMediumLow
Corner inset for ends (attachment)−22 bends, −1 crossing, but 3 merged ends−12 bends0noneVery small (hours)Low
Titles (names and tabs)−12 bends−10 bends0noneSmallLow (tabs), medium (state names)
Labels not forcing bends (state)−59 bends, +3 crossings−26 bends0noneLarge (3–5 days)Medium to high
Labels, no reservation at all−69 bends, +8 crossingsnot usable+8−20% diagram area—High
All realistic levers but labels3 crossings, 0 overlaps, 207 bends−2as box growth
All realistic levers3 crossings, 0 overlaps, 185 bends−2as 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).

VariantComponentPackageStateTotal
Baseline5 / 0 / 1480 / 0 / 340 / 0 / 1595 / 0 / 341
Labels free: no waypoint, no label obstacles5 / 0 / 1480 / 0 / 343 / 0 / 1008 / 0 / 282
Labels, no reservation in the layout5 / 0 / 1480 / 0 / 348 / 0 / 9013 / 0 / 272
Labels hybrid, raw5 / 0 / 1480 / 0 / 340 / 1 / 1485 / 1 / 330
Labels hybrid, never worse per diagram5 / 0 / 1480 / 0 / 340 / 0 / 1335 / 0 / 315
Names ignored as obstacles only5 / 0 / 1480 / 0 / 340 / 0 / 1595 / 0 / 341
Names and tabs ignored everywhere5 / 0 / 1460 / 0 / 280 / 0 / 1555 / 0 / 329
Titles realistic: tab ends, state names aside5 / 0 / 1480 / 0 / 280 / 0 / 1555 / 0 / 331
Exact state header obstacles5 / 0 / 1480 / 0 / 340 / 0 / 1595 / 0 / 341
Slides: corner inset 8, ends 8 apart4 / 0 / 1360 / 0 / 340 / 0 / 1494 / 0 / 319
Slides: ends 8 apart only4 / 0 / 1400 / 0 / 340 / 0 / 1554 / 0 / 329
Slides: corner inset 8 (12 on rounded states)5 / 0 / 1360 / 0 / 340 / 0 / 1595 / 0 / 329
Box growth N = 15 / 0 / 1020 / 0 / 320 / 0 / 1515 / 0 / 285
Box growth N = 25 / 0 / 940 / 0 / 320 / 0 / 1255 / 0 / 251
Box growth N = 45 / 0 / 820 / 0 / 200 / 0 / 1115 / 0 / 213
Titles + corner inset + growth N = 23 / 0 / 900 / 0 / 260 / 0 / 1193 / 0 / 235
Titles + corner inset + growth N = 43 / 0 / 780 / 0 / 200 / 0 / 1093 / 0 / 207
All realistic, labels included3 / 0 / 780 / 0 / 200 / 0 / 923 / 0 / 190
All realistic, never worse per diagram3 / 0 / 780 / 0 / 200 / 0 / 873 / 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:

PlacementCollide with somethingFar from their line (over 12 px)Ambiguous (another line at least as near)
Baseline, reserved spots003
Free routing, placed after9022
No reservation, placed by the existing placer16011

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-relations and language-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.

NBends savedBoxes grownArea addedShare of box area
1562027,520 px²0.9%
2903450,816 px²1.6%
41285197,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.

CauseComponentPackageStateTotal
Label (reservation, waypoint, obstacle)006262
Title (spans narrowed by tabs and names)26412
Attachment: corner inset and gap120214
Attachment: port policy (inner straight, outer jogs)320032
Box size5881581
Crowding by other lines0000
Side choice: a Z where a clear L fits1161128
Rank or ordering: a box or label in the way1222236
Rank or ordering: ends misaligned with no competing edge01910
Necessary: fan-in or fan-out on one side17112856
Defect or limit40610
Total14834159341

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

DiagramLinesCause
component/introduction-01 (Order Platform)Billing to Event Bus crosses the SQL dependency from Order Service to PostgresRank 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 linesThe same diagram as above.
component/example-simple (Compiler Pipeline)invoke from Driver to Code Generator crosses SSA from OptimizerAttachment 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 linesThe same diagram as above.
component/gallery-services-01 (Hexagonal service)Use Cases to the repo port crosses Use Cases to Domain ModelAttachment 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.

  1. 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 core after 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 → Levelled and Closed → Opening → Open, but puts near target floor on the next call line and lets the returns to Idle and Closed cross. The hybrid keeps the straight lines, but in TCP rcv ACK floats 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::metrics over the router’s polylines. A merged pair of ends, two different connectors leaving one point along one line, was counted separately, because overlaps excuses 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 Placer allows 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.

Layout pipeline Parser Compound Layout Slot Allocation Router Nudging Label Placement SVG Writer L over Z model rectangles terminals polylines polylines polylines

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:

CountSequenceActivity
Crossings and overlapping runsbetween messagesbetween flows and note links
Bendsnot counted: a self message always has twoevery flow
Lines through shapesmessages through heads, notes, refs, tabsflows through nodes and notes
Shape overlapstabs or notes over execution bars, shapes on shapes, shapes across frame edgesnodes and notes on each other or across partition dividers (fork bars may span)
Labels on linesmessage labels and guards within 2px of a message, lifeline, frame edge, arrowhead or dotguards within 2px of a flow, link, divider or arrowhead
Labels on shapes and labelsagainst heads, bars, notes, refs, tabsagainst nodes, notes, lane heads
Text overflownames in heads, note and ref text, operators in tabsnames in nodes, note text, lane names
Uneven gapsrows of consecutive one-line messages with nothing betweenstraight 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.

From chapter to page «input» Chapter Markdown mdBook Book Loader HTML Renderer mdbook-drumlin drumlin crate «output» Published Site IPreprocess IRender hook inline SVG

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:

Client Server IGreeting

Fence options

FenceOutput
```drumlinSource, then diagram
```drumlin,render-onlyDiagram alone
```drumlin,errorSource, 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 a drumlin,error example 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 Dockerfile compiles drumlin, builds the book and copies the static result into nginx.

Roadmap

AreaState
Component diagrams, with interfaces, ports and delegationPrototype
Packages as containersPrototype
Package diagrams with «import», «merge» and containmentPrototype
Sequence diagrams with fragments, activations, creation and destructionPrototype
State diagrams with composite states, regions and pseudostatesPrototype
Activity diagrams with decisions, loops, forks, signals, objects and partitionsPrototype
Notation audit against UML 2.5.1Done for all five kinds
JSON diagnosticsUnder consideration
Pluggable layout engines, and layout research itemsNot scheduled

Known limitations

All kinds:

  • No diagram has a UML frame, such as sd or pkg. 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 par cover 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).