Every pattern in this module is described with a diagram, and every design interview eventually reaches a whiteboard. You do not need to draw formal UML — almost nobody does any more. You need to read it, and the readable subset is small enough to learn in one sitting.
Two diagram types carry almost all the weight. A class diagram shows structure: what exists and how it is connected. A sequence diagram shows behaviour: what happens, in what order. Most confusion about a design comes from trying to answer a behaviour question with a structure diagram.
The class box
A class is a rectangle in up to three compartments: the name, the data, the operations.
classDiagram
class PaymentGateway {
<<interface>>
+charge(amountInCents, currency) Receipt
+refund(receiptId) Receipt
}
class StripeGateway {
-apiKey: string
-timeoutMs: int
+charge(amountInCents, currency) Receipt
+refund(receiptId) Receipt
-signRequest(body) string
}
class Checkout {
-gateway: PaymentGateway
+placeOrder(cart) Order
}
PaymentGateway <|.. StripeGateway
Checkout --> PaymentGatewayThree things are worth naming in that picture.
The visibility markers. + is public, - is private, # is protected. They are a claim about what other code is allowed to depend on — the public rows are the promise, the private rows are the mechanism.
The <<interface>> stereotype. Angle-quoted text above a name says what kind of thing this is rather than what it is called. You will also meet <<abstract>> and <<enumeration>>.
What is deliberately absent. Checkout names PaymentGateway, not StripeGateway. That single arrow is the entire claim of the design: checkout can be tested and can change providers without editing. Read the arrows first; the compartments are detail.
Five relationships, and the one distinction that matters
| Notation | Name | Read it as |
|---|---|---|
| Solid line, hollow triangle | Inheritance | "is a kind of" — subclass extends class |
| Dashed line, hollow triangle | Realisation | "promises to provide" — class implements interface |
| Solid line, open arrow | Association | "knows about" and holds a reference |
| Solid line, hollow diamond | Aggregation | "has, but does not own" — the part outlives the whole |
| Solid line, filled diamond | Composition | "has and owns" — the part dies with the whole |
| Dashed line, open arrow | Dependency | "uses briefly" — a parameter, a return type, a local |
classDiagram
class Order
class OrderLine
class Customer
class TaxCalculator
Order *-- OrderLine
Order o-- Customer
Order ..> TaxCalculatorThe distinction people get wrong is aggregation versus composition, and it is a lifetime question, not an importance question. Delete an order and its lines are meaningless, so lines are composition — filled diamond. Delete an order and the customer carries on existing and holding other orders, so the customer is aggregation — hollow diamond. If you cannot say which, use plain association; a wrong diamond misleads, an absent one merely omits.
Dependency is the weakest and the most overlooked. Order never stores a TaxCalculator; it receives one, uses it and forgets it. That dashed arrow is why the two can be released independently, and it is often the relationship a good design is trying to reach.
Multiplicity
Numbers at the ends of a line say how many. 1, 0..1, 1..*, *, or an exact 2.
classDiagram
class Playlist
class Track
Playlist "1" o-- "0..*" TrackRead it from each end separately: one playlist holds zero or more tracks, and a track belongs to one playlist. Multiplicity is where real design questions hide — 0..* in both directions means you have a join table and a set of ordering questions you have not answered yet.
Sequence diagrams: the same design over time
A class diagram cannot tell you what happens when someone clicks Pay. A sequence diagram can. Participants sit across the top, time runs downwards.
sequenceDiagram
participant C as Checkout
participant G as PaymentGateway
participant R as OrderRepository
participant E as EventBus
C->>G: charge(amount, currency)
activate G
G-->>C: Receipt
deactivate G
C->>R: save(order)
R-->>C: ok
C-)E: publish(order.placed)
Note over E: subscribers run independentlyA solid arrowhead is a call and the caller waits. charge blocks checkout until a receipt comes back, which is why a slow gateway is a slow checkout.
A dashed arrow is a return. It is not a new call; it is the answer to the one above it.
An open arrowhead is asynchronous — the caller does not wait. publish returns immediately and the subscribers run on their own. That single difference in arrowhead is the difference between "the warehouse notification failed and the customer saw an error" and "the warehouse notification failed and nobody noticed".
The vertical bar is an activation. It shows how long a participant is busy, and stacked bars are where latency accumulates.
Sequence diagrams also carry alt for branches and loop for repetition — but a diagram that needs six of them is trying to be source code, and the source code is better at it.
Which diagram answers which question
| Question | Diagram |
|---|---|
| What can change without breaking what? | Class |
| Who owns this data, and what happens when it is deleted? | Class, with diamonds and multiplicity |
| Why is this request slow? | Sequence, reading the activation bars |
| What happens if the third call fails? | Sequence, with the failure branch actually drawn |
| Which of these is the extension point? | Class, looking for the interface |
What a diagram cannot tell you
It is a claim, not a proof. A diagram is drawn from intention. The code may already disagree, and no diagram tells you it has drifted.
Error paths are invisible unless drawn. Most sequence diagrams show the happy path only, and most production incidents live in what they omitted.
Nothing about performance or concurrency is implied. Two participants side by side may run in parallel, in sequence, or on the same thread. Only the arrows say, and only if you drew them carefully.
Naming is not modelled. A box labelled Manager passes every validity check UML has and tells the reader nothing.
Check your understanding
Common mistakes
Answering a behaviour question with a class diagram. "Why is this slow" and "what happens if it fails" are never visible in structure.
Using diamonds to mean importance. They mean lifetime. If deleting the whole should not delete the part, the diamond is hollow or absent.
Drawing every method and field. A diagram exists to make one point. Show the participants and the relationships that make that point, and leave the rest to the code.
Omitting multiplicity. It is short to write and it is where the real questions are — nullability, ordering, and whether you have a join table.
Drawing only the happy path. The failure branch is the interesting one, and the one that turns into an incident.
Letting the diagram outlive the code. A confidently wrong diagram is worse than none, because people trust it. If it is not maintained, date it or delete it.
Practice
Sketch, in words, the class diagram for a notification system that can send by email, SMS or push; where a user has notification preferences; and where every send attempt is recorded. State every relationship type and every multiplicity, and justify the two you were least sure about.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Decide lifetime for each relationship before you decide the symbol.
Show a strong answer
The boxes: Notification (what is being sent), Channel as an interface with EmailChannel, SmsChannel and PushChannel realising it, User, NotificationPreference, and SendAttempt.
The relationships:
EmailChannel,SmsChannel,PushChannelrealiseChannel— dashed line, hollow triangle. Each promises the same operation, which is what lets the sender holdChanneland know nothing else.NotificationServicedepends onChannel— dashed open arrow, not a solid one, if the channel is passed in per send rather than stored. This is the relationship that makes the service testable.User"1" composes "0..*"NotificationPreference— filled diamond. A preference has no meaning without its user and should be deleted with them.Notification"1" aggregates "1"User— hollow diamond. The notification references a recipient who long outlives it.Notification"1" composes "1..*"SendAttempt— filled diamond, at least one attempt because a notification that was never attempted is not yet a notification.
The two I was least sure about, and how I decided:
Notification to User. The temptation is composition, because a notification always has a user. But composition is about lifetime, not obligation: deleting a notification must not delete the user. Hollow diamond, and the "always has one" part is expressed by the multiplicity 1, not by the symbol.
SendAttempt — composition of the notification, or an independent audit record? It depends on a requirement the diagram forces you to confront: if attempts feed a retention-limited audit log that is purged on its own schedule, they are not owned by the notification, and modelling them as composition will produce a delete cascade that silently destroys audit history. I chose composition on the assumption that attempts are part of the notification's own record, and I would flag that assumption explicitly — this is exactly the kind of question drawing the diagram is supposed to surface.
A sequence diagram for "place order" shows: Checkout →(solid)→ Inventory reserve; Inventory --> Checkout ok; Checkout →(open)→ EventBus publish. A reviewer says the inventory call should be asynchronous too, "for performance". Argue both sides and give a verdict.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Ask what the caller can honestly tell the user once the call no longer blocks.
Show a strong answer
The case for making it asynchronous: reserving inventory is the slowest step, it blocks the user's response, and its failures — a timeout, a slow lock — become checkout failures. Removing it from the critical path improves the response time directly and stops one dependency's availability from capping checkout's.
The case against, which is the stronger one: the arrowhead is not a performance setting, it is a claim about what the response means.
- Reserving inventory is a precondition for the order being valid. If it no longer blocks, checkout must return "order received" rather than "order confirmed", because it genuinely does not know yet.
- Failure now has nowhere to go. With a synchronous call, an out-of-stock item is an error the user can act on. With an asynchronous one it arrives after the user has left, and someone must build the compensation — an apology email, a refund, a cancellation — that did not exist before.
- The rest of the system inherits the change. Anything downstream that assumed a confirmed order had reserved stock is now wrong, and nothing in the diagram flags it.
Verdict: keep it synchronous, and treat the latency as a latency problem. Make the reservation faster, put a timeout and a circuit breaker around it, or narrow what is reserved at checkout time. Those attack the actual complaint without changing what a successful checkout promises.
What would change the verdict: a business decision to accept oversell — common where stock is plentiful and a rare apology is cheaper than a slow checkout. That is a product call, not a diagram call, and if it is made, the response text, the compensation path and the downstream assumptions all have to change with it.
The general rule worth taking away: synchronous versus asynchronous is a question about the meaning of the response, and performance is a consequence of the answer rather than a reason for it.
Key takeaways
- Class diagrams answer structural questions; sequence diagrams answer behavioural ones. Most confusion is a behaviour question asked of a structure diagram.
- Read the arrows before the boxes. Which interface a class points at is usually the entire design claim.
- Diamonds encode lifetime, not importance — filled means the part dies with the whole, hollow means it does not, and no diamond is better than a wrong one.
- Multiplicity is where the real questions hide: nullability, ordering and join tables.
- On a sequence diagram the arrowhead is the highest-information mark — it decides whether latency accumulates and whether failure is visible.
- A diagram is a claim, not a proof. It omits error paths, says nothing about concurrency, and drifts from the code silently.
- On a whiteboard, use fewer symbols and say the relationship aloud. Only the arrow directions are worth getting exactly right.
Further reading
- Martin Fowler, UML Distilled, third edition (Addison-Wesley, 2003) — the readable subset, and the argument for using less of it.
- Grady Booch, James Rumbaugh and Ivar Jacobson, The Unified Modeling Language User Guide, second edition (Addison-Wesley, 2005).
- Object Management Group, OMG Unified Modeling Language Specification, version 2.5.1 (2017) — the normative source for the notation.
- Simon Brown, Software Architecture for Developers (Leanpub, 2012) — the C4 model, a lighter alternative for system-level diagrams.
- Eric Evans, Domain-Driven Design (Addison-Wesley, 2003) — on models as shared language rather than documentation.