Back to the map

Module 03 · Design notation · Lesson 3 of 4

State diagrams

Modelling objects with a lifecycle: states, events, transitions, guards and actions. Why bookings, orders and machines need explicit states, turning a diagram into code, and rejecting illegal transitions.

CoreShould-knowMid 11 min Updated Nov 2026

Why it matters

Many objects in a design are not just data; they move through a lifecycle. An order is placed, paid, shipped, delivered or cancelled. A seat is free, held, sold. An elevator is idle, moving up, moving down, doors open. When those states are implicit, spread across booleans like isPaid and isShipped, impossible combinations creep in (shipped but not paid) and every method needs defensive checks. Making the state machine explicit, on paper and then in code, is one of the highest-value moves in a low-level design answer and the most common gap in otherwise good ones.

The idea

A state machine describes an object as being in exactly one state at a time. Events (method calls, messages, timeouts) trigger transitions to other states. A transition can have a guard (a condition that must hold) and an action (something done as it happens). Any event not allowed in the current state is rejected.

FreeHeldConfirmedCancelledhold(user)expire / releaseconfirm [not expired, paid]cancel [before show]seat releasedevent [guard] / action
A seat booking lifecycle. Each arrow is an allowed transition with its event and guard; anything not drawn, such as confirming an expired hold, is illegal.

Reading and drawing one

ElementDrawn asExample
StateRounded boxHeld
Initial stateFilled dot with an arrow to the first state→ Free
Final stateCircled dotArchived
TransitionArrow labelled event [guard] / actionconfirm [hold not expired] / charge card
Self-transitionArrow from a state back to itselfextendHold [extensions under 2]
Timed eventafter(duration)after(10 min) from Held to Free

From diagram to code

For small machines, an enum of states plus a transition check in each method is clear and enough:

enum BookingStatus { HELD, CONFIRMED, CANCELLED, EXPIRED }

final class Booking {
    private BookingStatus status = BookingStatus.HELD;
    private final Instant holdExpiresAt;

    void confirm(Instant now, PaymentResult payment) {
        require(status == BookingStatus.HELD, "only held bookings can be confirmed");
        if (now.isAfter(holdExpiresAt)) { status = BookingStatus.EXPIRED; throw new HoldExpired(); }
        require(payment.succeeded(), "payment must succeed");
        status = BookingStatus.CONFIRMED;
    }
    void cancel(Instant now, Instant showStart) {
        require(status == BookingStatus.CONFIRMED || status == BookingStatus.HELD, "nothing to cancel");
        require(now.isBefore(showStart), "cannot cancel after the show starts");
        status = BookingStatus.CANCELLED;
    }
}

For machines where each state behaves very differently (a vending machine that handles coins, selection and dispensing differently in each state), the State pattern gives each state its own class implementing a common interface, and the object delegates to its current state. It removes long switch statements at the cost of more classes; the Patterns stage covers it in detail. A transition table (current state, event to next state) is a third option, good when the machine is data-driven or configurable.

Rules that keep state machines honest

  • One status field, not many booleans. status = SHIPPED cannot contradict itself; isPaid=false, isShipped=true can.
  • Reject illegal transitions loudly. Throw a specific exception; never silently ignore an event that is not valid in the current state.
  • Guard with the information the object owns. Expiry, counts and timestamps live on the object, and an injected clock makes timed transitions testable.
  • Persist the state and protect transitions under concurrency. Two users confirming the same hold at once is a race; a version column or conditional update (UPDATE … WHERE status = 'HELD') makes only one win.
  • Record history when it matters. An audit trail of transitions (who, when, from, to) is often a requirement for orders and payments.

Trade-offs

Explicit state machines add upfront modelling and, with the State pattern, extra classes. They remove whole categories of bugs (impossible combinations, actions in the wrong state) and make behaviour reviewable at a glance. Over-modelling trivial lifecycles as elaborate machines is the opposite mistake; an object with two states and one transition needs a boolean, not a diagram.

In an interview

Whenever an entity has a lifecycle, draw its states early: "A Booking is Held, then Confirmed or Expired; a Confirmed booking can be Cancelled before the show. Each method checks the current state and rejects illegal transitions, and confirm uses a conditional update so two concurrent confirmations cannot both succeed." This is frequently the moment an interviewer decides a candidate is strong.

Check yourself

An Order class has booleans isPaid, isShipped and isCancelled. Support finds orders that are both cancelled and shipped. What modelling change prevents this class of bug?

State diagrams · Backplane