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.

Watch it evolve1 / 8
The problem

A print shop's jobs

The printer creates a job for each file and prints them in order. Each job has a status.

80
Start
Stage 1
Final

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.

*«enum»JobStatusQUEUEDPRINTINGPAUSEDDONECANCELEDPrintJob− id: int− status: JobStatus~ id(): int~ status(): JobStatus~ isWaiting(): boolean~ start()~ pause()~ resume()~ finish()~ cancel()− move(JobStatus, JobStatus)~ name(JobStatus): StringPrinter− queue: Deque<PrintJob>− current: PrintJob− nextId: int~ submit(): PrintJob~ startNext(): PrintJob~ paperOut()~ refilled()~ finishCurrent()

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 job isWaiting();
  • opt [job is waiting] around start(), which only happens when the guard holds.

Operator

Means

Guard

alt

Alternatives: at most one of the boxed parts runs

One per part, or else

opt

Optional: the boxed part runs or nothing does

One condition

loop

The boxed part repeats

A condition, or bounds such as loop(1, 3)

par

The boxed parts may run at the same time

Optional

break

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.

QueuedCanceledPrintingPausedDonestartpaper outrefilledfinishcancelcancelQueuedCanceledPrintingPausedDonestartpaper outrefilledfinishcancelcancel

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:

Status set from outsideJava
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.

Every arrow on the diagram is a methodJava
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

1. You want to show how the printer skips a canceled job when it starts the next one. Which diagram fits?
2. In a sequence diagram, what does a dashed arrow back to the caller mean?
3. The state diagram has no arrow from Done to Canceled. What does that mean for the code?
4. What does the transition label cancel [not started] / refund say?

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, opt and alt add 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.