ERD & UML diagrams
How to draw and read the two diagram families software teams still use: entity-relationship diagrams for data (Chen, crow's foot, IDEF1X, and how each mark becomes a PostgreSQL constraint) and UML 2.5.1 for code (class, sequence, state machine, activity, use case, component and deployment diagrams), with the TypeScript each class-diagram symbol becomes and the Mermaid, PlantUML, DBML and D2 that draw them from text. Schema design itself is in DB design & schemas; system-level views (C4) are in Software architecture; the structures these diagrams usually depict are in Design patterns.
Which diagram
Pick the diagram from the question you need answered, and draw one question per diagram.
| Question | Diagram | Notes |
|---|---|---|
| What data exists and how is it related? | ERD, conceptual or logical | the one diagram almost every team keeps |
| What tables, keys and constraints ship? | ERD, physical (crow's foot) | generate it from the live schema; don't hand-maintain it |
| What types exist and what holds a reference to what? | UML class | a domain model, or the shape of one pattern |
| Who calls whom, in what order, for one scenario? | UML sequence | APIs, auth flows, retries, anything async |
| What states can one thing be in, and what moves it? | UML state machine | every status column deserves one |
| What steps run, where do they branch and run in parallel? | UML activity | workflows across people or services |
| Who uses the system for what? | UML use case | a scoping index; the value is in the written use cases |
| What runs where, talking over what? | UML deployment, or C4 container/deployment | infrastructure reviews |
| How is the system split into parts with interfaces? | UML component, or C4 component | module and service boundaries |
| How does the whole system fit its world? | C4 system context | not UML; see Software architecture |
The strongest case against all of this: most teams don't use UML, and do fine. In Marian Petre's ICSE 2013 interview study of 50 professional developers, 35 used no UML at all, 11 used it selectively, 3 generated code from it, 1 drew it after the fact, and none used it wholeheartedly. The selective users drew class, sequence and activity diagrams, informally, to think and to explain. That is the defensible use: Martin Fowler's three modes are UML as sketch (to communicate; incomplete on purpose), as blueprint (complete enough to code from) and as programming language (executable models). Sketch by default, blueprint only at boundaries that are expensive to change (public APIs, schemas, protocols), and treat the third as a historical curiosity.
| Rule | Why |
|---|---|
| Title, scope and date on every diagram | a diagram without a scope gets read as the whole system |
| One notation per diagram, with a legend if it isn't standard | mixing Chen and crow's foot, or UML and boxes-and-arrows, makes every mark ambiguous |
| Keep it to one screen (roughly 5–12 boxes) | past that, split by scenario or by zoom level |
| Label every line | an unlabeled line says only "something is related" |
| Store the source next to the code, as text where possible | diffs in review, and it gets updated with the change that breaks it |
| Generate what can be generated (physical ERDs, dependency graphs) | hand-drawn copies of machine facts go stale first |
ERD levels
The same model is drawn at three levels of detail for three audiences. Most arguments about "the ERD" are two people looking at different levels.
| Level | Shows | Leaves out | M:N relationships | Audience |
|---|---|---|---|---|
| Conceptual | entities, relationships, cardinality, business names | keys, foreign keys, types | drawn as-is | domain experts, product |
| Logical | every attribute, primary and foreign keys, nullability, normalized to 3NF | DBMS types, indexes, storage | resolved into associative entities | analysts, developers |
| Physical | tables, columns and types, constraints, indexes, schemas, partitions | business narrative | junction tables | developers, DBAs, migration reviews |
| Term | Meaning |
|---|---|
| Entity (type) | a kind of thing with identity: customer, order (an instance is one customer) |
| Attribute | a property of an entity: simple or composite, single- or multivalued, stored or derived |
| Key | attributes whose values identify one instance; the chosen one is the primary key |
| Relationship | an association between entities, with a verb phrase in each direction |
| Degree | how many entity types a relationship joins: binary (2), ternary (3), n-ary |
| Cardinality | the maximum number of instances on one side per instance on the other: 1 or many |
| Participation (optionality, modality) | the minimum: 0 (partial, optional) or 1 (total, mandatory) |
| Weak entity | an entity identified only together with its owner: an order line within an order |
| Associative entity | a relationship promoted to an entity, usually because it has attributes: a membership with a role |
| Identifying relationship | the child's primary key includes the parent's key; non-identifying: it is an ordinary foreign key |
Say every relationship as two sentences, one per direction, with "each", a verb and a quantity: "each customer places zero or more orders" and "each order is placed by exactly one customer". If either sentence sounds wrong to the domain expert, the diagram is wrong.
ER notations
| Notation | Entity | Relationship | Maximum | Minimum | Where you meet it |
|---|---|---|---|---|---|
| Chen (1976) | rectangle; double for a weak entity | diamond; double for an identifying one | 1, N, M beside the lines | double line: total participation | textbooks, conceptual models |
| Chen (min,max) | as Chen | as Chen | (min,max) beside each entity | the min | Elmasri & Navathe, exams |
| Crow's foot (Information Engineering) | box listing attributes | line with a verb phrase | bar = one, fork = many (mark touching the box) | circle = zero, bar = one (next mark out) | ERwin, Lucidchart, dbdiagram, Mermaid, PlantUML, most tools |
| Barker (Oracle CASE*Method) | rounded box; # key, * mandatory, o optional attribute | line in two halves, one per end | fork = many | solid half = mandatory, dashed half = optional | Oracle SQL Developer Data Modeler |
| IDEF1X (FIPS 184) | square corners: independent; rounded: dependent | solid = identifying, dashed = non-identifying | dot = zero or more, P = one or more, Z = zero or one, a number = exactly that | diamond at the parent end = nullable foreign key | US government, ERwin |
| UML class diagram | class box | association line | 1, *, n..m at the far end | lower bound of the multiplicity | software models; see Class diagrams |
Look across vs look here. In crow's foot, UML and Chen's 1:N, the mark at an entity's end tells how
many of that entity relate to one instance on the other end: read from the far entity, across the
line. Chen's (min,max) is the opposite: the pair beside an entity says how many times that entity
takes part in the relationship. Customer (0,N) and Order (1,1) say the same thing as crow's foot
||──o<, written on the other ends. Translating between the two families without flipping the ends is
the most common ERD error there is.
Chen introduced the model in "The Entity-Relationship Model: Toward a Unified View of Data" (ACM TODS, March 1976). Gordon Everest drew the "inverted arrow", now called crow's foot, the same year, for maximums only; the circle and bar for minimums came later, and Information Engineering (James Martin and Clive Finkelstein) spread the full notation. Richard Barker's variant shipped with Oracle's CASE*Method (1990). IDEF1X became FIPS 184 in December 1993 and was withdrawn as a federal standard on September 2, 2008; IEEE 1320.2 carries it on.
Crow's foot
| End | Reads | UML | Mermaid (left / right) | Enforced in SQL by |
|---|---|---|---|---|
──┼┼ | exactly one | 1 | || / || | NOT NULL foreign key on the other table |
──o┼ | zero or one | 0..1 | |o / o| | nullable foreign key, or UNIQUE foreign key on the other table |
──┼< | one or many | 1..* | }| / |{ | nothing declarative: a deferred constraint trigger (Recipes) |
──o< | zero or many | 0..* | }o / o{ | the foreign key on this table, nothing more |
- The far end of the line answers the sentence: in
customer ||──o< order, theo<at the order end reads "each customer places zero or many orders"; the||at the customer end reads "each order belongs to exactly one customer". - Physical ERDs (ERwin, Mermaid, PlantUML) draw identifying relationships solid and non-identifying ones dashed. In Barker notation a dashed half means optional instead. Say which convention a diagram uses.
- Attributes go in the box, one per line, keys marked
PK,FK,UQ. Foreign key columns belong on logical and physical diagrams only: in a conceptual model the line is the relationship.
Chen
| Mark | Means |
|---|---|
| rectangle / double rectangle | entity / weak entity |
| diamond / double diamond | relationship / identifying relationship of a weak entity |
| oval, underlined name | attribute, key attribute |
| oval, dashed underline | partial key (discriminator) of a weak entity: unique only within its owner |
| double oval | multivalued attribute: becomes a child table |
| dashed oval | derived attribute: computed, not stored |
| oval with ovals hanging off it | composite attribute: its parts become columns |
| single / double line | partial / total participation |
1, N, M on the lines | cardinality ratio, read across |
Chen is verbose, and that is its use: relationship attributes, ternary relationships and weak entities are first-class, so it is the best notation for arguing about a conceptual model before tables exist. Nobody draws a 40-table physical schema in it.
Supertypes & subtypes
Enhanced ER (EER) adds specialization: a supertype with subtypes that inherit its attributes and relationships, drawn as lines from the supertype to a circle and on to each subtype.
| Constraint | Mark | Means |
|---|---|---|
| disjoint | d in the circle | an instance is at most one subtype (a payment is a card or a transfer) |
| overlapping | o in the circle | an instance can be several (a person who is both employee and customer) |
| total | double line from the supertype | every instance is some subtype |
| partial | single line | an instance can be just the supertype |
Crow's foot tools draw subtypes as boxes nested in the supertype or joined by an "is a" line; UML uses
generalization (hollow triangle) with {disjoint, complete} or {overlapping, incomplete}. The tables
follow one of Fowler's three mappings:
| Mapping | Tables | Good | Bad | Fits |
|---|---|---|---|---|
| Single table | one table and a kind column; subtype columns nullable | no joins; one foreign key target | many nulls; a CHECK per kind keeps them honest | disjoint, few subtype columns |
| Class table | one supertype table plus one per subtype whose primary key is also a foreign key to it | normalized; NOT NULL where it belongs | a join per read; "exactly one subtype row" needs a trigger | many subtype columns |
| Concrete table | one table per concrete subtype, supertype columns repeated | no joins within a type | cross-type queries need UNION; nothing can reference "any subtype" | types rarely queried together |
Polymorphic references to "any subtype" are covered in DB design & schemas.
From ERD to tables
| ER construct | Becomes |
|---|---|
| strong entity | a table; its key becomes the primary key (or a surrogate key plus UNIQUE on the natural key) |
| weak entity | a table whose primary key is the owner's key plus the partial key; foreign key ON DELETE CASCADE |
| 1:N relationship | a foreign key on the N side; NOT NULL if every N-side row must have a parent |
| 1:1 relationship | a foreign key with UNIQUE on the side with total participation, or one merged table |
| M:N relationship | a junction table; primary key = both foreign keys; relationship attributes become its columns |
| ternary relationship | a table with one foreign key per participant |
| multivalued attribute | a child table: owner foreign key plus value, both in the primary key |
| composite attribute | one column per part (or a composite type) |
| derived attribute | not stored: a view, a generated column or the query |
| supertype / subtype | one of the three mappings in Supertypes & subtypes |
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- ||..o{ : mandatory parent, non-identifying
customer_id bigint NOT NULL REFERENCES customers,
-- |o..o{ : optional parent
ship_address_id bigint REFERENCES addresses,
status text NOT NULL DEFAULT 'pending',
placed_at timestamptz NOT NULL DEFAULT now()
);
-- ||--|{ : identifying; the parent's key is in the PK
CREATE TABLE order_items (
order_id bigint REFERENCES orders ON DELETE CASCADE,
line_no int,
product_id bigint NOT NULL REFERENCES products,
quantity int NOT NULL CHECK (quantity > 0),
unit_price_cents int NOT NULL,
PRIMARY KEY (order_id, line_no)
);A foreign key enforces the parent side of a relationship. It can't enforce a minimum of one on the child
side (|{: every order has at least one item), nor a mandatory 1:1 in both directions: those need a
deferred constraint trigger or application code, and the diagram should say which.
| Trap | Model | Symptom | Fix |
|---|---|---|---|
| Fan trap | division 1:N branch, division 1:N staff | which branch does this person work at? The path fans out through division | relate the entities directly: division 1:N branch 1:N staff |
| Chasm trap | branch 1:N staff, staff 0..1:N property | a property nobody manages has no path to its branch | add the missing relationship: branch 1:N property |
| Ternary split into binaries | supplier–part, part–project, supplier–project | which supplier supplied which part to which project is lost | keep the ternary: one table with three foreign keys |
| Relationship as attribute | orders.customer_name | renames and typos split one customer into several | a foreign key to the entity |
| M:N left on a physical diagram | a line with forks at both ends between two tables | no table to put it in | draw the junction table |
UML at a glance
UML is an OMG standard: Booch, Rumbaugh and Jacobson merged their methods at Rational in 1994–96; OMG adopted UML 1.1 in November 1997, UML 2.0 in 2005, and the current version, 2.5.1, in December 2017 (ISO/IEC 19505:2012 is UML 2.4.1). UML 2.5 defines 14 diagram types.
| Kind | Diagram | Shows | Use it for |
|---|---|---|---|
| Structure | Class | types, attributes, operations, relationships | domain models, pattern structure, design reviews |
| Structure | Object | instances and links at one moment | explaining a tricky class diagram with an example |
| Structure | Package | namespaces and their dependencies | layer and module dependency rules |
| Structure | Component | replaceable parts and their provided/required interfaces | module and service boundaries |
| Structure | Composite structure | the internal parts, ports and connectors of one classifier | rare outside embedded and systems work |
| Structure | Deployment | nodes, execution environments, artifacts, paths | infrastructure |
| Structure | Profile | stereotypes that extend UML itself | tool builders |
| Behavior | Use case | actors, their goals, the system boundary | scoping |
| Behavior | Activity | steps, decisions, concurrency, who does what | workflows, business processes |
| Behavior | State machine | the lifecycle of one object, driven by events | anything with a status |
| Interaction | Sequence | messages between participants over time, for one scenario | APIs, protocols, async flows |
| Interaction | Communication | the same messages, numbered, drawn on the links between objects | rarely; a sequence diagram reads better |
| Interaction | Timing | lifeline states along a time axis, with time constraints | hardware, real-time, protocols with deadlines |
| Interaction | Interaction overview | an activity diagram whose nodes are interactions | stitching sequence diagrams together; rare |
Shared notation, valid on any diagram:
| Element | Notation | Example |
|---|---|---|
| Stereotype | guillemets before the name | «interface», «enumeration», «entity», «use» |
| Constraint | text in braces, natural language or OCL | {ordered}, {total = sum of lines} |
| OCL invariant | context type inv: expression | context Order inv: self.lines->notEmpty() |
| Note | dog-eared rectangle, dashed line to what it explains | free text |
| Keyword | as a stereotype, built in | «include», «extend», «create» |
| Diagram frame | rectangle with a pentagon tag in the corner | sd checkout, stm Order, act Checkout |
Class diagrams
Members
| Syntax | Example | Means |
|---|---|---|
| visibility | + public, - private, # protected, ~ package | who may use the member |
| attribute | - status: OrderStatus = Pending | visibility name: Type [multiplicity] = default {properties} |
| multiplicity on an attribute | - tags: string [0..*] | the attribute holds a collection; omitted means exactly one |
| derived | + /total: Money | computed from other values |
| static | + create(c: Customer): Order, underlined | class scope, not per instance |
| abstract | class or operation name in italics, or {abstract} | must be overridden |
| operation | + pay(in gw: PaymentGateway): Receipt | visibility name(direction param: Type, …): Return {properties}; directions in (default), out, inout |
| read-only | {readOnly} | can't change after initialization |
| identifier | {id} | part of the object's identity |
| redefinition / subset | {redefines items}, {subsets items} | a subclass narrows an inherited property |
| template class | Repository<T> with T in a dashed box on the corner | a generic type |
{ordered} | {unique} | Collection | TypeScript |
|---|---|---|---|
| no | yes (the default) | set | Set<T> |
| yes | yes | ordered set | an array kept free of duplicates |
| no | {nonunique} | bag | Map<T, number> of counts, or an array |
| yes | {nonunique} | sequence | T[] |
An attribute and a navigable association end are the same thing in the UML metamodel (a property).
Convention: draw a line to classes that appear on the diagram, and write an attribute for value types
(string, Money, OrderId). Drawing both is the same fact twice.
Relationships
| Relationship | Line | End mark | Means | Ask | TypeScript |
|---|---|---|---|---|---|
| Association | solid | open arrowhead = navigable; × = not navigable; none = unspecified | instances are linked | does one hold a reference to the other? | a field |
| Aggregation (shared) | solid | hollow diamond at the whole | whole–part, parts may be shared | none: the spec says its semantics "varies by application area and modeler" | a field holding something passed in |
| Composition | solid | filled diamond at the whole | the part belongs to one whole at a time and is normally deleted with it | is the part ever shared or does it outlive the whole? If not: composition | created inside, never handed out for keeping |
| Generalization | solid | hollow triangle at the parent | is-a; the child inherits | could a child be used wherever the parent is? | extends |
| Realization | dashed | hollow triangle at the interface | implements a contract | — | implements |
| Dependency | dashed | open arrowhead at the supplier | uses, temporarily | does a change to the supplier force a change here? | a parameter, local, or import used in a method |
| Association class | dashed line from a class to an association | — | the link itself has attributes | is there data about the pair (a role, a date)? | a class holding both references; a junction table |
| Qualified association | small box at the source end with a key | — | the key selects the target | look-up by key? | Map<Key, Target> |
- A composite's end has multiplicity
1or0..1, never more: a part has at most one owner. - Don't draw aggregation. It has no defined semantics, so every reader supplies their own. Use composition when ownership matters, a plain association otherwise.
- Multiplicities are written at the far end and read across, like crow's foot:
Order "*" → "1" Customeris "each order has one customer; each customer has many orders". Write one on every end: a missing one reads as 1 to some people and "unknown" to others. - Navigability arrows say who holds the reference. Draw them when the direction is a design decision (it usually is), not on every line.
- Dependency stereotypes:
«use»,«create»,«call»,«instantiate»,«import»(packages). - Interfaces can be drawn as a class box with
«interface», or as a ball (lollipop) the class provides and a socket (half circle) a class requires.
UML to TypeScript
The diagram above as code. TypeScript's types are structural, so a class that never says implements
still satisfies an interface it matches: draw realization only for contracts you declare on purpose.
// «enumeration»: a union of literals (or an enum)
type OrderStatus =
| "Pending" | "Paid" | "Shipped" | "Canceled";
// «interface»: no runtime trace; checked by shape
interface PaymentGateway {
charge(m: Money): Receipt;
}
// realization: `implements` makes the contract explicit
class StripeGateway implements PaymentGateway {
constructor(private readonly apiKey: string) {}
charge(m: Money): Receipt {
return callStripe(this.apiKey, m);
}
}
class OrderLine {
constructor(
private qty: number,
private unitPrice: Money,
readonly product: Product, // association, 1
) {}
subtotal(): Money {
return times(this.unitPrice, this.qty);
}
}
class Order {
// composition: created here, never handed out to keep
readonly #lines: OrderLine[] = []; // 1..* {ordered}
private status: OrderStatus = "Pending";
private constructor(
private readonly id: OrderId, // {readOnly}
private customer: Customer, // association, 1
) {}
// underlined in UML: a static (class-scope) member
static create(c: Customer): Order {
return new Order(newId(), c);
}
// derived (/total): computed, not stored
get total(): Money {
return sum(this.#lines.map((l) => l.subtotal()));
}
add(p: Product, qty: number): void {
this.#lines.push(new OrderLine(qty, p.price, p));
}
// dependency «use»: a parameter, not a field
pay(gw: PaymentGateway): void {
// the 1..* lower bound is an invariant to check
if (this.#lines.length === 0) throw new Error("empty");
gw.charge(this.total);
this.status = "Paid";
}
}| UML | TypeScript | Watch out |
|---|---|---|
+ public | public (the default) | — |
- private | private (compile time) or #field (enforced at run time) | UML # means protected; a JavaScript #field is private |
# protected | protected | — |
~ package | no equivalent; an unexported module member is closest | — |
{readOnly} | readonly | shallow: the array behind a readonly field is still mutable |
0..1 | T | undefined, or an optional property x?: T | — |
*, {ordered} | T[] or ReadonlyArray<T> | an array is a sequence: {unique} isn't enforced |
*, unordered unique | Set<T> | — |
| qualified association | Map<K, T> | — |
| abstract class or operation | abstract class, abstract m(): T | — |
| «interface» | interface or a type alias | erased at run time; no instanceof |
template T | generics class Repo<T> | — |
Sequence diagrams
1/9
POST /checkout: the browser calls the API and waits. A synchronous call is a solid line with a filled arrowhead; the thin bars on the lifelines are execution specifications, the time each participant is busy.
| Element | Notation | Means |
|---|---|---|
| Lifeline | name : Type in a box, dashed line down | one participant; :Type for an anonymous instance |
| Actor | stick figure heading a lifeline | a human or external system |
| Execution specification | thin bar on the lifeline | the participant is busy (optional, but helps with nesting) |
| Synchronous message | solid line, filled arrowhead | a call; the sender waits |
| Asynchronous message | solid line, open arrowhead | a signal or event; the sender carries on |
| Reply | dashed line, open arrowhead | a return value, written name = value or just the value |
| Create | dashed line, open arrowhead, to a lifeline's head drawn lower down | the participant starts existing here |
| Destroy | × at the end of a lifeline | the participant stops existing |
| Found / lost message | from / to a filled dot | sender / receiver unknown or out of scope |
| Self-message | arrow looping back to the same lifeline | a call to one's own operation |
| Duration / time constraint | {≤ 2 s} between two marks; {t..t+3} | deadlines and timeouts |
| Gate | message ending on the frame's edge | the message crosses the diagram's boundary |
| State invariant | [condition] or a state name on the lifeline | must hold at that point |
| Fragment | Means |
|---|---|
alt | alternatives, each with a guard; at most one operand runs; [else] when no guard holds |
opt | one operand that runs only if its guard holds: an alt with one branch |
loop | repeats while the guard holds; loop(min, max); bare loop means 0 to unbounded |
break | if the guard holds, runs instead of the rest of the enclosing fragment, then leaves it |
par | operands run in parallel; their messages interleave in any order |
critical | no other messages may interleave on these lifelines: an atomic region |
neg | traces that must not happen |
assert | the only valid continuation |
strict | strict order across lifelines, top to bottom |
seq | weak order (the default): order per lifeline only |
ignore / consider | messages left out of / the only ones counted in the fragment |
ref | an interaction use: another sequence diagram inlined by name |
- One diagram per scenario; the happy path first, then one diagram per interesting failure.
- Name participants after roles in the design (
:OrderService), not after deployment units, unless the diagram is about deployment. - Leave out getters and trivial calls. A sequence diagram that shows every line of code is the code, drawn worse.
- Async is the point of most sequence diagrams worth drawing: show who waits, who doesn't, and where a reply can be lost.
State machine diagrams
| Element | Notation | Means |
|---|---|---|
| Initial pseudostate | filled dot | where a region starts; its one transition has no trigger or guard |
| Final state | dot in a ring | the region is done |
| State | rounded rectangle | a condition the object stays in while waiting for events |
| Transition | arrow labeled trigger [guard] / effect | on the trigger, if the guard holds, run the effect and move |
| Entry / exit action | entry / a, exit / a inside the state | runs on every way in / out |
| Do activity | do / a | runs while in the state; interrupted when it is left |
| Internal transition | trigger [guard] / effect inside the state | handled without leaving: no exit or entry actions run |
| Self-transition | arrow leaving and re-entering the same state | runs exit, then entry; timers restart |
| Completion transition | arrow with no trigger | fires when the state's do activity (or its regions) finish |
| Composite state | a state with a nested machine | substates share the outer transitions |
| Orthogonal regions | dashed line splitting a composite state | several substates active at once |
| Choice | diamond | branches on guards evaluated after the effects so far (dynamic) |
| Junction | small filled dot | chains transitions; guards evaluated before any of it runs (static) |
| History | H (shallow), H* (deep) | re-enter the substate that was last active |
| Fork / join | bar | enter / leave several orthogonal regions together |
| Entry / exit point | small circle / circled × on the border | named ways into and out of a composite state |
| Terminate | × | the machine stops at once |
| Event | Written | Fires when |
|---|---|---|
| Signal | paymentReceived | an asynchronous signal arrives |
| Call | cancel() | an operation is called |
| Change | when(stock = 0) | the condition becomes true |
| Time, relative | after(30 min) | that long after entering the source state |
| Time, absolute | at(2027-01-01) | at that moment |
| Completion | no trigger | the state's own work is finished |
The UML state machine is David Harel's statechart (1987) with object-oriented semantics. XState implements statecharts for TypeScript with the same vocabulary (entry/exit actions, guards, delayed transitions, parallel states, history). A plain reducer is enough until timers, nesting or parallel regions appear; the Recipes show one.
Activity diagrams
| Element | Notation | Means |
|---|---|---|
| Initial node | filled dot | a token starts here |
| Activity final | dot in a ring | the whole activity ends, all flows with it |
| Flow final | circled × | this flow ends; others go on |
| Action | rounded rectangle | a step |
| Control flow | open arrow | the order of steps |
| Decision / merge | diamond | one out of several guarded flows / brings alternatives back together, without waiting |
| Fork / join | bar | splits into parallel flows / waits for all of them |
| Object node, pins | rectangle on a flow, small squares on an action | data passed between actions |
| Partition (swimlane) | a band with a name | who or what performs the actions in it |
| Send signal | convex pentagon | sends an event and goes on |
| Accept event / time event | concave pentagon / hourglass | waits for an event / for time to pass |
| Interruptible region | dashed rounded rectangle; lightning-bolt flow out of it | an event aborts everything inside |
| Call behavior | action with a rake symbol | runs another activity |
- Semantics are token flow, like a Petri net: a join waits for a token on every incoming flow, a merge passes each token straight through. A merge where a join was meant runs the next step twice.
- Every decision's guards must cover every case, and must not overlap; add
[else]. - For processes owned by the business (approvals, handoffs, SLAs), BPMN 2.0 (OMG 2011, ISO/IEC 19510:2013) has richer events and gateways and is what business analysts read. Activity diagrams fit software workflows; a plain flowchart fits a slide.
Use case diagrams
| Element | Notation | Means |
|---|---|---|
| Actor | stick figure (or a box with «actor») | a role outside the system: a person, an organization, another system |
| Use case | ellipse with a verb phrase | a goal an actor reaches through the system |
| Subject | rectangle named for the system | the boundary |
| Association | solid line actor–use case | the actor takes part |
| Generalization | hollow triangle between actors or use cases | a specialized actor or use case |
«include» | «extend» | |
|---|---|---|
| Arrow points | from the base to the included use case | from the extension to the base |
| Runs | always, at a set point in the base | only when its condition holds, at an extension point |
| Is the base complete without it? | no | yes |
| Use for | steps shared by several use cases (Pay) | optional or exceptional behavior (Apply coupon) |
The diagram is a table of contents. The requirements live in the written use cases: goal, actors,
preconditions, a numbered main success scenario and its extensions (Alistair Cockburn's format; a
template is in the Recipes). Two smells: a use case per screen or per CRUD
operation (functional decomposition, not goals), and «include»/«extend» chains deeper than one level.
Component & deployment diagrams
| Element | Notation | Means |
|---|---|---|
| Component | box with «component» or the component icon | a replaceable part behind interfaces: a module, a service |
| Provided interface | ball (lollipop) on a stick | what the component offers |
| Required interface | socket (half circle) on a stick | what it needs from elsewhere |
| Assembly connector | ball in socket | this provider satisfies that requirement |
| Port | small square on the border | a named interaction point |
| Delegation connector | arrow from a port to an inner part | the work is passed inside |
| Node | 3D box; «device» or «executionEnvironment» | hardware, a VM, a container runtime |
| Artifact | rectangle «artifact» with a page icon | a deployable file: a bundle, an image, a jar |
| Deployment | artifact drawn inside a node, or a dashed «deploy» arrow | where it runs |
| Communication path | line between nodes, labeled with the protocol | HTTPS, TCP 5432, AMQP |
| Package | folder with a tab | a namespace; «import» (public) and «access» (private) dependencies |
C4 covers the same ground with fewer rules: a container diagram is roughly a component diagram plus a deployment diagram, drawn as labeled boxes with a legend. Use the UML forms when your tools and readers already speak UML, or when interface wiring (ball and socket) is the point. A deployment diagram in PlantUML is in the Recipes.
Other UML diagrams
| Diagram | Reach for it when | Notation in one line |
|---|---|---|
| Object | a class diagram's multiplicities confuse people | boxes named order1 : Order (underlined) with slot values, joined by links |
| Communication | the links between objects matter more than the timing | objects joined by links; messages numbered 1, 1.1, 1.2 along them |
| Timing | a protocol has deadlines or a signal changes state over time | one lane per lifeline, its state as a step line along a time axis |
| Interaction overview | several sequence diagrams make up one flow | an activity diagram whose nodes are ref frames |
| Composite structure | the inside of one class is itself a small system | parts as boxes inside the class, connected through ports |
| Profile | you define stereotypes for a tool or a domain | «stereotype» classes extending «metaclass» with a filled-triangle arrow |
Diagrams as code
| Tool | Draws | Renders | Layout | Use it for |
|---|---|---|---|---|
| Mermaid | flowchart, class, sequence, state, ER, C4, Gantt and more | natively in GitHub, GitLab, Notion, Obsidian; mmdc CLI | automatic (dagre; ELK optional) | docs in Markdown; least control over layout |
| PlantUML | every UML diagram above, ER (IE notation), C4 via C4-PlantUML, Gantt | Java CLI or a server; IDE plugins | Graphviz dot (or the bundled Smetana port); sequence diagrams use their own engine (Teoz with a pragma) | complete UML; the most options |
| D2 | general diagrams, sql_table, class, sequence_diagram shapes | d2 CLI | dagre, ELK, or TALA (commercial) | good-looking architecture diagrams |
| DBML | database schemas | dbdiagram.io, dbdocs; @dbml/cli converts to and from SQL | automatic | ERDs that round-trip with DDL |
| Structurizr DSL | C4 models | Structurizr | automatic or manual | one model, many C4 views |
| Graphviz DOT | any directed graph | dot CLI, everywhere | dot, neato, fdp | generated diagrams (dependency graphs) |
| Mermaid class arrow | Means | Mermaid sequence arrow | Means |
|---|---|---|---|
<|-- | generalization | ->> | synchronous call |
*-- | composition (diamond at the left) | -->> | reply |
o-- | aggregation | -) | asynchronous message |
--> | association, navigable | -x | line ending in a cross (failed or lost) |
-- | link | -> / --> | solid / dotted line, no arrowhead |
..> | dependency | <<->> | two-way arrow (recent versions) |
..|> | realization | + / - after the arrow | activate / deactivate the target |
.. | dashed link | create participant, destroy | lifeline creation and destruction (Mermaid 10.3+) |
GitHub renders fenced mermaid blocks in Markdown, issues and pull requests; PlantUML and D2 need a CI
step, a server or an editor plugin. Render to SVG on the command line:
npx -p @mermaid-js/mermaid-cli mmdc -i erd.mmd -o erd.svg
java -jar plantuml.jar -tsvg deploy.puml
d2 --layout elk arch.d2 arch.svg
npx -p @dbml/cli dbml2sql schema.dbml --postgres -o out.sqlDiagrams from existing code and schemas:
| From | Tool | Gives |
|---|---|---|
| a live PostgreSQL database | DBeaver, DataGrip, pgAdmin's ERD Tool, SchemaSpy | physical ERDs |
| a SQL dump | sql2dbml (@dbml/cli) | DBML for dbdiagram.io |
| a Prisma schema | prisma-erd-generator (community) | a Mermaid ERD on every prisma generate |
| Python packages | pyreverse (ships with Pylint) | class and package diagrams as dot, puml or mmd |
| a TypeScript codebase | dependency-cruiser (--output-type dot) | module dependency graphs |
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Reading Chen (min,max) like crow's foot | every cardinality comes out reversed | (min,max) sits beside the entity it constrains; crow's foot marks sit at the far end |
| Two notations on one diagram | a Chen diamond between crow's-foot tables; UML diamonds on an ERD | pick one per diagram and add a legend |
| Foreign key columns on a conceptual ERD | the relationship appears twice, as a line and as an attribute | the line is the relationship; columns arrive in the logical model |
| Many-to-many lines on a physical ERD | there is nowhere to store the pairs | draw the junction table and its attributes |
| Minimum of one that nothing enforces | "every order has an item" on the diagram, empty orders in production | enforce it (deferred trigger, aggregate root) or draw o{ |
| Composition everywhere | every reference drawn as a filled diamond | composition only where the part's lifetime is the whole's |
| Aggregation at all | readers argue about what the hollow diamond means | plain association, or composition |
| Attribute and association for the same thing | customer: Customer in the box and a line to Customer | one or the other |
| Navigability arrows on every line, or none | direction of references unknowable or noise | arrows where the direction is a decision |
# read as private | a TypeScript #field drawn as UML protected | UML # is protected; a JavaScript #field is private (-) |
| A sequence diagram of every method call | 30 lifelines, 200 messages | one scenario, the interesting calls, ref for the rest |
| Sync arrowheads on async messages | readers assume the sender waits | open arrowhead for anything sent and not waited on |
| State diagram of steps, not states | "Validate", "Send email" as states | states are conditions you wait in; steps belong to an activity diagram |
| Merge drawn where a join was meant | the step after it runs twice | bar (join) to wait for all; diamond (merge) to take whichever comes |
«include» / «extend» backwards | arrows point the wrong way | include: base → included; extend: extension → base |
| A use case per screen | 40 ellipses of "Edit X", "Delete X" | one per actor goal; CRUD is one "Manage X" at most |
| Hand-maintained physical ERD | the diagram disagrees with the schema | generate it from the database or from the schema source |
Recipes
ERD in Mermaid
Renders on GitHub as-is. Relationship tokens are the crow's foot ends; -- is identifying (solid),
.. non-identifying (dashed).
erDiagram
CUSTOMER ||..o{ ORDER : places
CUSTOMER ||..o{ ADDRESS : has
ADDRESS |o..o{ ORDER : "ships to"
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT ||..o{ ORDER_ITEM : "sold as"
CUSTOMER {
bigint id PK
text email UK
text name
}
ORDER {
bigint id PK
bigint customer_id FK
bigint ship_address_id FK "nullable"
text status
}
ORDER_ITEM {
bigint order_id PK, FK
int line_no PK
bigint product_id FK
int quantity
}Class diagram in Mermaid
$ after an operation marks it static, * abstract; cardinalities go in quotes beside the arrow.
classDiagram
class PaymentGateway {
<<interface>>
+charge(m: Money) Receipt
}
class Order {
-OrderId id
-OrderStatus status
+total() Money
+add(p: Product, qty: number) void
+pay(gw: PaymentGateway) void
+create(c: Customer)$ Order
}
class OrderStatus {
<<enumeration>>
Pending
Paid
Shipped
Canceled
}
Order "*" --> "1" Customer
Order "1" *-- "1..*" OrderLine : lines
OrderLine "*" --> "1" Product
Order ..> PaymentGateway : «use»
StripeGateway ..|> PaymentGatewaySequence diagram in Mermaid
+ and - after an arrow open and close the target's activation bar.
sequenceDiagram
participant B as Browser
participant A as CheckoutApi
participant S as OrderService
participant G as Gateway
participant Q as Queue
B->>+A: POST /checkout
A->>+S: placeOrder(cart)
S->>S: validate(cart)
S->>+G: charge(total)
G-->>-S: result
alt approved
S-)Q: OrderPlaced
else declined
S-)Q: PaymentFailed
end
S-->>-A: order
A-->>-B: 201 CreatedState machine in Mermaid
stateDiagram-v2
[*] --> Pending
Pending --> Pending: pay [declined] / retry
Pending --> Paid: pay [approved]
Pending --> Canceled: cancel, after(30 min)
Paid --> Shipped: ship
Paid --> Canceled: cancel / refund
Shipped --> Delivered: deliver
Delivered --> [*]
Canceled --> [*]
Paid: Paid (entry / reserveStock)State machine as a reducer
One case per state, one if per outgoing arrow; anything unhandled is ignored. Effects (refund,
reserve stock, restart the 30-minute timer) run outside, from the old and new state.
type State =
| "Pending" | "Paid" | "Shipped"
| "Delivered" | "Canceled";
type Ev =
| { type: "pay"; approved: boolean }
| { type: "ship" | "deliver" | "cancel" | "timeout" };
export function next(s: State, e: Ev): State {
switch (s) {
case "Pending":
// [declined]: self-transition, timer restarts
if (e.type === "pay") return e.approved ? "Paid" : s;
if (e.type === "cancel" || e.type === "timeout") {
return "Canceled";
}
return s;
case "Paid":
if (e.type === "ship") return "Shipped";
if (e.type === "cancel") return "Canceled"; // refund
return s;
case "Shipped":
return e.type === "deliver" ? "Delivered" : s;
default:
return s; // final states accept nothing
}
}At least one child in PostgreSQL
|{ ("one or many") as a deferred constraint trigger: checked at commit, so the order and its items
can be inserted in either order inside one transaction.
CREATE FUNCTION order_has_items() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM order_items WHERE order_id = NEW.id
) THEN
RAISE EXCEPTION 'order % has no items', NEW.id;
END IF;
RETURN NULL;
END $$;
CREATE CONSTRAINT TRIGGER order_needs_items
AFTER INSERT ON orders
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE FUNCTION order_has_items();Deleting the last item of an existing order needs a second trigger, on order_items for DELETE, that
runs the same check against OLD.order_id.
ERD in DBML
For dbdiagram.io; @dbml/cli turns it into PostgreSQL DDL and back.
Table customers {
id bigint [pk, increment]
email text [unique, not null]
name text
}
Table orders {
id bigint [pk, increment]
customer_id bigint [not null, ref: > customers.id]
ship_address_id bigint [ref: > addresses.id]
status text [not null, default: 'pending']
}
Table order_items {
order_id bigint [ref: > orders.id]
line_no int
product_id bigint [not null, ref: > products.id]
quantity int [not null]
indexes {
(order_id, line_no) [pk]
}
}ERD in PlantUML
Information Engineering notation; * marks a mandatory attribute by convention, -- separates the key.
@startuml
hide circle
skinparam linetype ortho
entity customers {
* id : bigint <<PK>>
--
* email : text <<UQ>>
name : text
}
entity orders {
* id : bigint <<PK>>
--
* customer_id : bigint <<FK>>
ship_address_id : bigint <<FK>>
}
customers ||..o{ orders : places
@endumlDeployment diagram in PlantUML
@startuml
node "app-1" <<device>> {
node "Bun 1.3" <<executionEnvironment>> {
artifact "api.js" as api
}
}
node "db-1" <<device>> {
database "PostgreSQL 18" as pg
}
queue "orders" as q
api --> pg : TCP 5432
api --> q : AMQP
@endumlTables in D2
customers: {
shape: sql_table
id: bigint {constraint: primary_key}
email: text {constraint: unique}
}
orders: {
shape: sql_table
id: bigint {constraint: primary_key}
customer_id: bigint {constraint: foreign_key}
}
orders.customer_id -> customers.idUse case template
Cockburn's fields, trimmed. The extensions are where the requirements hide.
Use case: Place order
Primary actor: Customer
Scope: Shop (the system) Level: user goal
Stakeholders: Customer wants the goods at the shown price;
Shop wants payment before shipping.
Precondition: the cart holds at least one item
Guarantees: success: order Paid, stock reserved
minimal: no charge without an order
Trigger: Customer chooses "Check out"
Main success scenario
1. Customer confirms the cart and shipping address.
2. System prices the order and shows the total.
3. Customer pays (include: Pay).
4. System reserves stock and confirms the order.
Extensions
2a. Customer enters a coupon (extend: Apply coupon).
3a. Payment is declined: System keeps the order Pending
and offers to retry; after 30 minutes it is Canceled.
4a. An item went out of stock: System refunds and
notifies the Customer.Review checklist
[ ] title, scope, date and the question it answers
[ ] one notation; legend if anything is non-standard
[ ] every line labeled; every end has a multiplicity
[ ] cardinalities read aloud as two sentences each way
[ ] ERD level stated: conceptual, logical or physical
[ ] minimums of one: enforced, or not drawn as one
[ ] composition only where lifetimes are shared
[ ] sequence: one scenario; async drawn as async
[ ] state machine: states are waits, not steps
[ ] source stored next to the code, rendered in CIReferences
- OMG UML 2.5.1 specification (opens in a new tab): the normative text, notation annexes included
- Chen: The Entity-Relationship Model, Toward a Unified View of Data (ACM TODS, 1976) (opens in a new tab): the original ER paper
- FIPS PUB 184: Integration Definition for Information Modeling (IDEF1X) (opens in a new tab): the IDEF1X standard, 1993
- Marian Petre: UML in Practice (ICSE 2013) (opens in a new tab): how 50 professional developers did and didn't use UML
- Martin Fowler: UmlMode (opens in a new tab): sketch, blueprint, programming language
- uml-diagrams.org (opens in a new tab): every UML 2.5 diagram and element with examples
- David Harel: Statecharts, a visual formalism for complex systems (1987) (opens in a new tab): the basis of UML state machines
- XState documentation (opens in a new tab): statecharts in TypeScript
- OMG BPMN 2.0 (opens in a new tab): business process notation
- C4 model (opens in a new tab): system context, container, component and code views
- Mermaid: entity relationship diagrams (opens in a new tab), class diagrams (opens in a new tab), sequence diagrams (opens in a new tab) and state diagrams (opens in a new tab): syntax references
- GitHub Docs: creating diagrams (opens in a new tab): where Mermaid renders on GitHub
- PlantUML (opens in a new tab) and its layout engines (opens in a new tab): Graphviz, Smetana and the rest
- DBML documentation (opens in a new tab): syntax for dbdiagram.io and
@dbml/cli - D2 documentation (opens in a new tab): the D2 language, SQL tables and sequence diagrams
- Pyreverse (opens in a new tab): UML from Python code