Drawing UML class diagrams
Open in the interactive app: practice, quiz, mock interview →In one minute
- A class diagram shows static structure: the types, what each holds and offers, and how they relate. Ordering and timing belong to sequence and state diagrams.
- Each class is a box with name, attributes and operations, with visibility marks (
+,-,#,~) on members. - Six arrows carry most of the meaning: association, dependency, aggregation, composition, inheritance and realization. Each maps to a different code construct, lifetime and coupling cost.
- Multiplicity (
1,0..1,,1..,2..5) at each end of an association is a rule about the object graph, not decoration. Something in the code or the database has to enforce it. - In an interview, draw the five to eight classes that carry the requirements, label arrows and multiplicities, and talk while you draw.
How it works
What the diagram is for
A class diagram drops method bodies, imports and error handling, and keeps what reviewers argue about: which classes exist, who owns whom, and which way dependencies point. Martin Fowler distinguishes UML as sketch, as blueprint and as programming language. Nearly all useful work today is sketching: draw to explore or explain, and stop once the picture has answered the question.
The class box
The top compartment holds the name, plus an optional stereotype such as <<interface>>, <<abstract>> or <<enumeration>>. The middle holds attributes in the form visibility name: Type [multiplicity] = default, for example - lines: OrderLine [1..*]. The bottom holds operations as visibility name(param: Type): ReturnType, for example + addLine(item: MenuItem, qty: int): void.
Static members are underlined and abstract ones italicised; Mermaid writes them with a trailing $ and *. Compartments may be left empty. A diagram that lists every getter is a diagram nobody reads.
Visibility
+ public, - private, # protected and ~ package map to Java's keywords directly. Python only has the _name convention, and Go's nearest fit is a capitalised (exported) versus lowercase name. The marks state the interface you intend, not what the language enforces, so the diagram is often the clearest record of that intent.
Multiplicity
A multiplicity is a range lower..upper. 1 is shorthand for 1..1, and for 0... It sits at the far end of an association from the object you are reading from: Customer "1" --> "*" Order reads "one customer has any number of orders; each order has exactly one customer".
Each bound means something in code. Lower bound 1: the constructor receives the other object. Lower bound 0: every reader handles absence. Upper bound 1: a single field. Upper bound *: a collection; a bound like 5 adds a check on every insert. The Association lesson enforces these when a link is made; this lesson's practice checks them over a whole object graph.
The six relationships
These are the arrows from the earlier lessons in this course, collected in one place with their notation:
| Relationship | Line and head | Mermaid | Reads as | Typical code | |
|---|---|---|---|---|---|
| Association | solid line, open arrowhead for navigability | A --> B | A knows B over time | field holding a reference or id | |
| Dependency | dashed line, open arrowhead | A ..> B | A uses B briefly | parameter, local variable, return type | |
| Aggregation | solid line, hollow diamond at the whole | A o-- B | A groups Bs that live on their own | collection of references to shared objects | |
| Composition | solid line, filled diamond at the whole | A *-- B | A owns Bs; they die with it | objects created and held only by A | |
| Inheritance | solid line, hollow triangle at the parent | `A <\ | -- B` | B is a kind of A | extends, subclassing, embedding as a crude substitute |
| Realization | dashed line, hollow triangle at the interface | `A <\ | .. B` | B fulfils the contract A | implements, structural interface satisfaction |
Dependency and association differ in time: one call versus a stored reference. Aggregation and composition differ in lifetime: deleting a playlist keeps its tracks, deleting an order deletes its lines. Realization and inheritance differ in what is shared: a contract only, versus contract plus implementation and state. The UML specification itself leaves the semantics of shared aggregation to the application domain, so use the hollow diamond only when the team agrees what it means.
Using it in an interview
In a low-level design round the interviewer reads your diagram before your code. A sequence that works:
- Restate requirements as a few use cases and underline the nouns. Nouns with state or rules become classes; pure values (a price, a seat number) become attributes.
- Draw named boxes, then relationships and multiplicities before attributes, because that is where the decisions are.
- Ask per arrow: if this object is deleted, what else goes? That separates composition, aggregation and association.
- Add interfaces only where you expect variation, each with a reason you can say out loud.
- Walk one use case through the diagram. If you cannot trace it, a relationship is missing.
Five to eight classes is enough. Twenty-five boxes in 45 minutes signals you modelled the nouns, not the problem.
Diagrams in the age of generated code
Code assistants over-produce class models: an interface per class, a manager per noun, links in both directions. Asking for a Mermaid class diagram of generated code is a cheap review step, since arrows in every direction reveal coupling faster than reading files. Diagram-as-code also lets the picture change in the same pull request as the code.
The same shape elsewhere
A class diagram is a schema for an object graph. An entity-relationship diagram says the same about tables (1 on the one side becomes a NOT NULL foreign key, to a join table), and JSON Schema says it about messages. The shape is "trust but verify": a declared structure plus a validator that checks real data against it.
Down the stack
Layer 1The contract
A class diagram promises structural facts that hold whenever a caller can observe the system: an Order reaches its lines, every OrderLine belongs to exactly one Order, CardPayment satisfies PaymentMethod. Each is testable: multiplicities as invariants after every public operation, realization as "compiles where the interface is expected", composition as "after deleting the whole, no part is reachable". It promises nothing about ordering or timing; that belongs in sequence and state diagrams.
Layer 2The mechanism
In the UML 2.5 metamodel an Association element has two member ends, each a Property with lower and upper bounds, navigability, and an aggregation kind of none, shared or composite. Generalization and InterfaceRealization are separate elements. Tools exchange the model as XMI, an XML format.
Mermaid is lighter: it parses classDiagram text into classes, members and relations with optional cardinality labels, and a layout engine (dagre by default) renders SVG. It checks neither multiplicities nor interface methods; the text is only a drawing. Enforcement lives in code: constructors for lower bounds of 1, guarded add methods for upper bounds, database constraints, and graph validators like the practice below.
Layer 3The machine
Each arrow has a memory shape. On a 64-bit machine an upper bound of 1 is one 8-byte pointer (4 with JVM compressed oops), and 0..1 adds a null check at every read. An upper bound of * in Go is a 24-byte slice header plus a backing array; an empty CPython list is 56 bytes plus 8 per slot.
Composition can be stored by value, which aggregation cannot. A Go struct holding Address rather than *Address puts the part inside the whole: one allocation, often the same cache line, and the part cannot outlive the whole. Aggregation must be a pointer, because the part is shared.
Inheritance and realization show up in dispatch. A C++ object with virtual methods carries an 8-byte vtable pointer, and a virtual call is two dependent loads plus an indirect branch. A Go interface value is two words: an itab pointer (cached per type and interface pair) and a data pointer. JavaScript walks the prototype chain, made cheap by V8's inline caches. A dependency costs nothing in object layout because it lives on the stack for one call, which is the physical reason it is the loosest relationship.
Why it's built this way
- Code is too detailed to discuss structure, so teams need a smaller picture that keeps types and links and drops the rest.
- The links that matter differ in lifetime and coupling, so the notation needs distinct arrows for "uses briefly", "knows", "groups", "owns", "is a" and "fulfils".
- Counts drive data structures and constraints (field or collection, nullable or required), so each end carries a multiplicity.
- Pictures drift from code, so modern practice keeps diagrams small and stores them as text next to the code, where review catches drift.
Diagram
A food-delivery order model that uses all six relationships, visibility marks and multiplicities.
classDiagram
class Customer {
+String id
-String email
+placeOrder(cart) Order
}
class Order {
+String id
-OrderStatus status
+addLine(item, qty) void
+total() int
}
class OrderLine {
-int qty
+subtotal() int
}
class MenuItem {
+String name
#int priceCents
}
class DeliveryZone {
+String name
}
class Courier {
+String id
}
class PaymentMethod {
<<interface>>
+charge(amountCents) Receipt
}
class CardPayment {
-String token
+charge(amountCents) Receipt
}
class Discount {
<<abstract>>
+apply(cents) int
}
class PercentOff {
-int percent
+apply(cents) int
}
class PricingService {
+quote(order) int
}
Customer "1" --> "*" Order : places
Order "1" *-- "1..*" OrderLine : contains
OrderLine "*" --> "1" MenuItem : refers to
DeliveryZone "1" o-- "*" Courier : covers
Order "*" --> "0..1" Courier : assigned
Order "*" --> "1" PaymentMethod : paid by
PaymentMethod <|.. CardPayment
Discount <|-- PercentOff
PricingService ..> Order : prices
PricingService ..> Discount : appliesWorked example
An interviewer asks for the model behind a food-delivery checkout. The requirements are: customers place orders of one or more menu items; an order is paid by card or wallet; a courier from the order's zone is assigned after payment; discounts may apply.
Of the nouns, quantity and price are values and become attributes, and receipt is only returned, so it stays a return type. OrderLine appears because "one or more menu items, with quantities" is a many-to-many association that carries data.
The ownership questions settle the diamonds. Delete an order and its lines go: composition, 1 to 1..*. Delete a zone and its couriers still work elsewhere: aggregation. Delete an order and the customer remains: plain association. PricingService only reads an order during quote, so it is a dependency, not a field.
The multiplicities turn into code directly:
class Order:
def __init__(self, order_id, customer, payment, first_line):
self.id = order_id
self.customer = customer # "1": required in the constructor
self.payment = payment # "1": required
self.courier = None # "0..1": readers must handle None
self._lines = [first_line] # "1..*": cannot start empty
def remove_line(self, line):
if len(self._lines) == 1:
raise ValueError("order needs at least one line")
self._lines.remove(line)Now the cost. With 200,000 open orders of 3 lines each in Go, []OrderLine (32-byte lines by value) is one backing array per order. []*OrderLine adds 600,000 separate heap objects for the garbage collector to trace. Both are under 10 MB, so choose by semantics; at 50 million lines in a pricing batch, value composition is one cache-friendly scan instead of 50 million pointer chases.
In the interview, spend about 5 minutes on requirements, 10 on the diagram, 20 coding Order, OrderLine and PaymentMethod, and the rest walking through "place order, pay, assign courier".
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Whiteboard sketch | Fast; invites discussion; no tooling | Lost after the meeting; no review history | Interviews, early design sessions |
| Diagram-as-code (Mermaid, PlantUML) | Lives in the repo; diffable; reviewed with code | Layout control is limited; still drifts if nobody updates it | Design docs and READMEs for long-lived modules |
| Diagram generated from code | Always matches the code | Shows everything, so it hides the important parts | Onboarding to an unfamiliar codebase, auditing coupling |
| Full UML model with tools and XMI | Precise; supports code generation and model checks | Heavy; few teams keep it current | Regulated or safety-critical domains that require models |
| No diagram, code only | Nothing to maintain | Structure is invisible until someone reads every file | Small modules with two or three types |
Pitfalls in production
- Diagrams drift from code and mislead new engineers. Keep them as small text files in the repository and delete the ones nobody updates.
- Every noun becomes a class and every class gets an interface. Require a named reason for each interface, such as two implementations or a test seam.
- Composition is drawn where the code shares the part, so deleting one whole corrupts another. Check who constructs the part and whether anything else holds it.
- Multiplicities are drawn and never enforced.
1..*means nothing ifremove_linecan empty the list; check in the mutating method and in the database when there are several writers. - Two-way arrows by default store one fact twice. Draw navigability only in the directions a query needs.
Leadership lens
- Ask for diagrams of the part that is changing, at five to fifteen boxes, and read the arrows before the boxes: which way do dependencies point, and does any domain class depend on infrastructure?
- Treat multiplicities as requirements. "An order can have several couriers now" turns
0..1into*: a migration, a change to every reader and a new concurrency rule, and the estimate should say so. - For agent-generated designs, ask the agent for the class diagram first. It is cheaper to reject a design at ten boxes than at two thousand lines.
Check yourself
A
PlaylistholdsTrackobjects that appear in many playlists and survive playlist deletion. Should the diagram show composition?Show answer
No. Shared parts that outlive the whole are aggregation (or a plain association). Composition requires that each part belongs to one whole at a time and dies with it.
What changes in code when an association end changes from
1to0..1?Show answer
The constructor no longer requires the object, the field becomes nullable or optional, and every reader must handle absence. In a database, the foreign key column drops its NOT NULL constraint.
Why is a dependency weaker than an association, in terms of what is in memory?
Show answer
A dependency exists only for the duration of a call, as a parameter or local on the stack. An association is a stored field, so the two objects stay linked and a change to the target's structure affects the holder for its whole lifetime.
Your Go code has
type Order struct { lines []OrderLine }. Which diagram relationship does that most naturally express, and why?Show answer
Composition. The lines are stored by value inside the order's backing array, so nothing else can hold the same line by reference and the lines become unreachable when the order does.
Watch
- UML Class Diagram Tutorial (Lucid Software): a clear walkthrough of boxes, visibility, multiplicity and the relationship types with one running example.
- UML 2.0 Class Diagrams (Derek Banas): a fast tour of the notation, drawn step by step.
Free resources
- Class diagrams (Mermaid documentation): the syntax used on this site for members, visibility, cardinality and every relationship arrow.
- UML Specification 2.5.1 (OMG): the official standard, including the Association, Property and AggregationKind definitions.
- UmlAsSketch (Martin Fowler): why most valuable UML use is quick, selective sketching.
- UmlMode (Martin Fowler): the sketch, blueprint and programming-language modes, and what each costs.
- Class diagram (PlantUML): the other popular diagram-as-code syntax, useful when Mermaid's layout is not enough.
- Class diagram (Wikipedia): background on the notation and a summary of the relationship types.