Low-level designCore8 min
UML sequence and state diagrams
A class diagram shows what exists. A sequence diagram shows one interaction in order: who calls whom, and what comes back. A state diagram shows one object's life: the states it can be in and the events that move it between them.
A print shop's jobs
The printer creates a job for each file and prints them in order. Each job has a status.
1 · Three views
Structure, one interaction, one object's life
A print shop takes files at the desk and prints them in the order they arrive. Customers can cancel a job while it waits, or while it's paused, and the printer pauses a job when it runs out of paper. The code has two classes, a Printer with its queue and a PrintJob with a status, plus an enum JobStatus.
The class diagram: the printer creates and keeps its jobs, and each job has a status. It says nothing about the order of calls, or about which status can follow which.
Diagram | It shows | Draw it when |
|---|---|---|
Class | The types, their members and how they relate | Always: it's the map of the design |
Sequence | One interaction, as calls in time order | The order matters, or three or more objects take part |
State | One object's states and the events between them | An object has a lifecycle with rules about what may happen when |
2 · Sequence diagrams
One interaction, read from top to bottom
Job 2 was canceled at the desk. When the printer finishes job 1, staff start the next one. Here is that interaction as a sequence diagram: each object gets a column with a dashed lifeline under it, time runs down the page, and every arrow is a message.
Printer.startNext() skips the canceled job and starts the next one that's waiting.
A sequence diagram is the right tool when the order is the point. Here it shows something the class diagram can't: the printer asks each job whether it's waiting before starting it, so a canceled job is passed over.
Element | Drawn as | Means |
|---|---|---|
Lifeline | A box with a dashed line down from it | One object, for the length of the interaction |
Call | Solid arrow, filled head | A method call; the caller waits for it |
Asynchronous message | Solid arrow, open head | Sent without waiting, such as an event or a queued task |
Reply | Dashed arrow back to the caller | The return value, written on the arrow |
Activation bar | A thin box on a lifeline | The object is busy handling a call |
The figures on these pages leave out activation bars to stay readable on a phone; full diagrams usually draw them.
3 · Loops and choices
Fragments: loops, options and alternatives
Real code loops and branches, and a sequence diagram can show that with a combined fragment: a box around some messages, labeled with an operator and a guard in square brackets. The startNext() above is really a loop over the queue with a choice inside it:
loop [queue not empty]around asking the next jobisWaiting();opt [job is waiting]aroundstart(), which only happens when the guard holds.
Operator | Means | Guard |
|---|---|---|
| Alternatives: at most one of the boxed parts runs | One per part, or |
| Optional: the boxed part runs or nothing does | One condition |
| The boxed part repeats | A condition, or bounds such as |
| The boxed parts may run at the same time | Optional |
| Run this instead of the rest of the enclosing fragment | One condition |
Use fragments sparingly. A diagram with nested loops and alternatives is harder to read than the code it describes; draw the main path, and the one branch that matters.
4 · State diagrams
One object's life, as states and transitions
A print job is always in exactly one of five states. A state diagram draws each state as a rounded box and each allowed change as an arrow, a transition, labeled with the event that causes it. A filled dot marks where every job starts, with an unlabeled arrow to its first state, and a ringed dot marks where its life ends.
A print job's life. Every arrow is an allowed change; anything not drawn, such as canceling a job that's done, is not allowed.
The full label of a transition is event [guard] / action, and each part after the event is optional. The guard is a condition that must hold for the transition to fire, and the action is what happens on the way. A job could be drawn with cancel [not started] / refund to say that only an unstarted job is refunded.
The diagram's real value is what it leaves out. There's no arrow from Done to Canceled, and no arrow from Canceled to Printing. That's a rule about the code, and the next section shows what happens when the code doesn't follow it.
5 · The change
Any code can set any status
The first version gives PrintJob a setter, and the printer and the desk set the status directly. It's short, and each line looks harmless:
class PrintJob {
private final int id;
private JobStatus status = JobStatus.QUEUED;
PrintJob(int id) {
this.id = id;
}
int id() {
return id;
}
JobStatus status() {
return status;
}
void setStatus(JobStatus status) {
this.status = status;
}
static String name(JobStatus s) {
return s.name().toLowerCase();
}
}
class Printer {
private final Deque<PrintJob> queue = new ArrayDeque<>();
private PrintJob current;
private int nextId = 1;
// Each submitted file becomes a job, at the back of the queue.
PrintJob submit() {
PrintJob job = new PrintJob(nextId++);
queue.addLast(job);
return job;
}
// Starts the next job in line.
PrintJob startNext() {
current = queue.pollFirst();
if (current != null) current.setStatus(JobStatus.PRINTING);
return current;
}
void paperOut() {
current.setStatus(JobStatus.PAUSED);
}
void refilled() {
current.setStatus(JobStatus.PRINTING);
}
void finishCurrent() {
current.setStatus(JobStatus.DONE);
current = null;
}
}Run the same day at the shop through it: job 2 is canceled, job 1 prints with a pause for paper, and then the desk tries to cancel job 1.
Job 2: canceled Job 1: printing Job 1: paused Job 1: printing Job 1: done Job 2: printing Job 2: done Job 1: canceled
Job 2 printed after it was canceled, and job 1 was marked canceled after it was done. Neither change is on the diagram.
Nothing crashed. The printer took the next job in line without asking about it, and the desk overwrote a finished job's status, so the shop would refund a job it printed. Two transitions the diagram forbids happened, because the rules lived in the diagram and nowhere in the code.
6 · The fix
Each transition becomes a method
Replace the setter with one method per event: start(), pause(), resume(), finish() and cancel(). Each one checks the current state and refuses a transition the diagram doesn't have. The printer now asks isWaiting() before starting a job.
class PrintJob {
private final int id;
private JobStatus status = JobStatus.QUEUED;
PrintJob(int id) {
this.id = id;
}
int id() {
return id;
}
JobStatus status() {
return status;
}
boolean isWaiting() {
return status == JobStatus.QUEUED;
}
void start() {
move(JobStatus.QUEUED, JobStatus.PRINTING);
}
void pause() {
move(JobStatus.PRINTING, JobStatus.PAUSED);
}
void resume() {
move(JobStatus.PAUSED, JobStatus.PRINTING);
}
void finish() {
move(JobStatus.PRINTING, JobStatus.DONE);
}
// Only a job that isn't printing or finished can be canceled.
void cancel() {
if (status != JobStatus.QUEUED && status != JobStatus.PAUSED) {
throw new IllegalStateException("it is " + name(status));
}
status = JobStatus.CANCELED;
}
private void move(JobStatus from, JobStatus to) {
if (status != from) throw new IllegalStateException("it is " + name(status));
status = to;
}
static String name(JobStatus s) {
return s.name().toLowerCase();
}
}Job 2: canceled Job 1: printing Job 1: paused Job 1: printing Job 1: done Job 3: printing Job 3: done Cancel job 1: refused, it is done
The canceled job is skipped, and canceling a finished job is refused.
Read the code next to the diagram: every arrow is a method, and every method checks the state its arrow leaves from. When a new rule arrives, such as a job that can be held for approval, the diagram changes first, and the code follows it arrow by arrow.
7 · Drawing well
Draw the interaction or the lifecycle that's hard to see
- Sequence: one scenario per diagram. "Start the next job when one is canceled" is a diagram; "how the printer works" is not.
- Sequence: name real methods. Messages that match the code, like
startNext(), keep the diagram honest. - State: draw objects with rules about time. Orders, bookings, payments, jobs and accounts usually have one. A value object with no lifecycle doesn't need one.
- State: check every missing arrow. Each pair of states without an arrow is a rule; make sure the code refuses it.
In an interview, a quick sequence diagram for the hardest use case and a state diagram for the main entity often say more than a large class diagram.
8 · In practice
A snack machine's states
The vending machine practice problem is built around a lifecycle: waiting for coins, holding money, dispensing, refunding. Sketch its state diagram first, then make each arrow a method that checks where the machine is.
Design the vending machine, starting from its states.
Check yourself
4 questions
Takeaways
Remember this
- A sequence diagram shows one interaction in time order: lifelines, calls as solid arrows, replies as dashed ones.
- Fragments such as
loop,optandaltadd repetition and choices, each with a guard in square brackets. - A state diagram shows one object's states and the events that move it, from a start dot to an end dot.
- A transition's full label is
event [guard] / action. - Every missing arrow in a state diagram is a rule; the code should refuse that change.