Free Interactive Course · Design Patterns

State Design Pattern

Let an object change its behaviour when its state changes — so illegal transitions stop being an if-statement you forgot.

Behavioural Patternsmediuma.k.a. Objects for States
ShareXLinkedIn
In one sentence

Allow an object to alter its behaviour when its internal state changes, so that it appears to change its class.

01

The problem State solves

The problem

An order moves through a life: pending, paid, shipped, delivered, and possibly cancelled or refunded. Five operations act on it — pay, ship, deliver, cancel, refund — and what each one means depends entirely on where the order is right now.

Cancelling a pending order just marks it cancelled. Cancelling a paid order has to refund the card and release the stock. Cancelling a shipped order isn't a cancellation at all — it's a return, and it needs a label and a warehouse notification. Cancelling a delivered order should be refused outright.

So cancel() becomes a switch on status. So does ship(), and refund(), and the three methods the reporting code added. Six methods, each switching over the same five statuses, each having to remember which transitions are legal — and nothing anywhere states the rules in one place. Someone adds a partially_shipped status for split deliveries, updates four of the six switches, and now a partially shipped order can be silently cancelled without a refund.

02

How the State pattern works

Make each state a class, and give it the whole behaviour of being in that state:

  1. Define a state interface with every operation the object supports: pay(), ship(), cancel().
  2. Write one class per state. PaidState.cancel() refunds and releases stock; DeliveredState.cancel() refuses. Each class is small and answers only for its own situation.
  3. The context (the order) holds a current state object and delegates every call to it. Its methods become one line each.
  4. Transitions happen by the state replacing itself on the context — context.setState(new ShippedState()) — so each state declares where it can go, and nowhere else has to know.
The switch didn't disappear; it turned into a lookup — and that's the point. The value isn't fewer branches, it's that all the behaviour for "being paid" now lives in one class instead of being scattered across six methods. Adding PartiallyShippedState becomes one new file, and in a language with exhaustive checking the compiler will tell you what you haven't handled. The version with six switches gives you no such warning — it just runs.
Order- state : OrderState«interface» OrderStatepay() · ship() · cancel()delegatesPendingStatePaidStateShippedStateeach state sets the next one on the contextorder.cancel() → state.cancel(order) → refund + release stockthe green arrows are the transition rules — declared by the states, not by the order
Participants. The Context (Order) holds a reference to the current Concrete State and delegates to it. Each state implements the same State interface and knows which state to move to next. Note the green arrows: transitions belong to the states, which is what lets you add a state without editing the context.
03

See it: the same call, four meanings

The order calls state.cancel(this) — one line, always the same line. Pick where the order currently is and watch which class answers, and how differently.

▶ Try it — cancel() in each state

An interactive state machine: calling cancel() on an order routes to PendingState, PaidState, ShippedState or DeliveredState, and each handles it differently — mark cancelled, refund and release stock, start a return, or refuse. The Order class calls the same single line in every case.

Now add Partially shipped. In this design that's one new class and the counter stays at zero edits to Order. In the version this replaced — six methods each switching on status — it was six edits, and the bug was always the one switch somebody missed, because nothing failed until an order with the new status hit that method in production.
04

State pattern code examples

One class per state, transitions declared by the states, and a context whose methods are one line each.

public interface OrderState {
    default void pay(Order order)    { throw new IllegalTransition("pay", this); }
    default void ship(Order order)   { throw new IllegalTransition("ship", this); }
    default void cancel(Order order) { throw new IllegalTransition("cancel", this); }
}

/** Default methods make ILLEGAL the default — a state opts in to what it allows. */
public final class PaidState implements OrderState {

    @Override public void ship(Order order) {
        warehouse.dispatch(order);
        order.setState(new ShippedState());     // the state names its successor
    }

    @Override public void cancel(Order order) {
        payments.refund(order.receipt());
        inventory.release(order.lines());
        order.setState(new CancelledState());
    }
}

public final class Order {
    private OrderState state = new PendingState();

    void setState(OrderState next) { this.state = next; }

    // The context's methods are one line each, forever.
    public void pay()    { state.pay(this); }
    public void ship()   { state.ship(this); }
    public void cancel() { state.cancel(this); }
}

// Java enums with abstract methods are the compact idiom for small machines,
// and give you serialisation and switch-exhaustiveness for free:
//
//     enum Status {
//         PAID    { void cancel(Order o) { refund(o); o.to(CANCELLED); } },
//         SHIPPED { void cancel(Order o) { startReturn(o); } };
//         abstract void cancel(Order o);
//     }
Read across the tabs: the classic class-per-state version is only one option, and often not the best one. TypeScript's discriminated union and C++'s std::variant give you what the pattern is really after — exhaustiveness, so adding a state breaks the build instead of shipping a hole — and both let each state carry only the data that state has. Python's tab shows the other honest alternative: when states hold no behaviour, a transition table is smaller, testable and printable as a diagram. Reach for classes when each state has rich behaviour of its own; reach for a union or a table when the machine is mostly bookkeeping.
05

How to implement State

  1. List the states and the events, and draw the legal transitions before writing code. Most state bugs are decided at this step, not in the implementation.
  2. Define the state interface with every operation the context supports.
  3. Make illegal the default: a base class or default method that throws, so each state opts in to what it permits.
  4. Write one class per state, holding only the data that state actually has.
  5. Decide where transitions live. In the state (flexible, and states then know each other) or in the context (centralised, but the context grows). GoF allows both; pick one and be consistent.
  6. Prefer returning the next state to mutating the context — pure transitions are trivially testable and can't leave a half-moved object behind.
  7. Make illegal transitions loud: an exception or an error return, never a silent no-op.
  8. If the machine is persisted, map states to stable stored values and write a test for every transition you claim to support.
06

When to use State — and when not to

Use it when an object's behaviour depends on its state and it changes behaviour at runtime; when several operations all contain the same large conditional on a status field; or when the legal transitions are business rules that deserve to be visible and tested rather than implied.

Where it goes wrong

A class per state for a two-state machine. On/off does not need an interface and two classes. A boolean and an if is the right answer, and the pattern only starts paying at three or four states with genuinely different behaviour.

States that know too much about each other. When transitions live in the states, PaidState names ShippedState, which names DeliveredState. That's usually fine, but it means adding a state can mean editing its neighbours — if that's happening constantly, move the transition table into the context.

Losing the machine's shape. Eight state classes in eight files describe a machine nobody can see. Keep a diagram, or a printable transition table, next to the code — and treat a state machine you can't draw as a bug.

Shared mutable data in states. If a state object holds data belonging to the context, transitioning either loses it or duplicates it. States should be small and, where possible, immutable.

Silent illegal transitions. A cancel() on a delivered order that quietly does nothing is far worse than one that throws — the caller believes it worked, and the divergence appears somewhere else entirely.

You want to…UseBecause
Change behaviour as an object's situation changes, over timeStateStates swap themselves and know the legal transitions.
Let the caller choose an interchangeable algorithmStrategyStrategies are independent and chosen from outside; states replace each other from within.
Split two independently growing hierarchiesBridgeBridge's implementor is chosen once; a state changes throughout the object's life.
Record what happened so it can be undoneCommandCommand captures the request; State captures the situation.
Share state objects across many contextsFlyweightStateless state objects are natural flyweights — one instance serves every order.
07

Quick check

🧠 Quick check
State and Strategy have identical class diagrams. What actually distinguishes them?

In the wild

JavaThread.State and the TCP connection state machine; Spring Statemachine for the explicit version with guards and listeners.
JavaScriptXState — statecharts with guards, nested states and a visualiser; also the browser's own XMLHttpRequest.readyState and promise states.
C#TaskStatus, and the Stateless library, which draws your machine as a DOT graph — a picture of the transitions is worth a lot in review.
GoRob Pike's lexer design, where a state is a function returning the next state function: type stateFn func(*lexer) stateFn, and the scanner is a loop.
Pythondjango-fsm for model state machines with transition guards, and asyncio task states (pending, running, cancelled, done).
C++Boost.SML and Boost.MSM for compile-time state machines, and the std::ios stream state bits (goodbit, failbit, eofbit).

Frequently asked questions

What is the difference between the State and Strategy patterns?
The class diagrams are identical; the dynamics are not. A Strategy is selected by the client, usually once, and the strategies are independent and unaware of each other. A State is swapped by the object itself in response to events, and the states typically know which state comes next — so together they encode a machine with legal and illegal transitions. Strategy answers "how should this be done?"; State answers "what is this object allowed to do right now?"
Should the transition logic live in the states or in the context?
GoF permits both, and the trade-off is real. Putting transitions in the states keeps each state self-contained and means adding a state doesn't touch the context — but states then name each other, so they're coupled to their neighbours. Putting them in the context (a transition table) makes the whole machine visible in one readable place and keeps states ignorant of each other — at the cost of a context that grows with every state. Small machines with rich behaviour: states. Large machines that are mostly bookkeeping: a table in the context.
Isn't the State pattern just a big switch statement in disguise?
The branch has to exist somewhere — the question is whether it exists once or once per method. The problem it solves is six methods each switching over the same five statuses, where adding a status means finding all six and where missing one fails silently at runtime. With the pattern, everything about being in a state lives in one class, and adding a state is one file. In TypeScript, C++ or a language with exhaustive matching you get more than tidiness: the compiler refuses to build until every switch handles the new state.
How many states before the pattern is worth it?
Roughly three or four with genuinely different behaviour, or the moment you have a second method switching on the same status field. Two states and one method is a boolean. The stronger signal is not the count but the duplication: when the same switch (status) appears in several places, the shape of the code is already telling you the states should be objects — or a table.
How should illegal transitions be handled?
Loudly, and by default. Make the base state throw for every operation so each concrete state has to opt in to what it permits — then a transition you forgot to implement fails immediately and obviously rather than silently doing nothing. In Go, return an error along with the unchanged state so a failed transition can't leave the object half-moved. What you must not do is let cancel() on a delivered order quietly return: the caller believes it worked, and the inconsistency surfaces days later somewhere unrelated.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.