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:
Define a state interface with every operation the object supports: pay(), ship(), cancel().
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.
The context (the order) holds a current state object and delegates every call to it. Its methods become one line each.
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.
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);
// }
from __future__ import annotations
from typing import Protocol
class OrderState(Protocol):
def pay(self, order: Order) -> None: ...
def ship(self, order: Order) -> None: ...
def cancel(self, order: Order) -> None: ...
class IllegalTransition(Exception):
pass
class BaseState:
"""Everything is illegal until a subclass says otherwise."""
def pay(self, order: Order) -> None: raise IllegalTransition("pay")
def ship(self, order: Order) -> None: raise IllegalTransition("ship")
def cancel(self, order: Order) -> None: raise IllegalTransition("cancel")
class Paid(BaseState):
def ship(self, order: Order) -> None:
warehouse.dispatch(order)
order.state = Shipped()
def cancel(self, order: Order) -> None:
payments.refund(order.receipt)
inventory.release(order.lines)
order.state = Cancelled()
class Order:
def __init__(self) -> None:
self.state: OrderState = Pending()
def pay(self) -> None: self.state.pay(self)
def cancel(self) -> None: self.state.cancel(self)
# Honest alternative: when the states have no behaviour of their own, a
# TRANSITION TABLE is smaller and easier to test than a class per state —
# and it can be printed, diffed and rendered as a diagram:
#
# TRANSITIONS = {
# ("pending", "cancel"): ("cancelled", release_reservation),
# ("paid", "cancel"): ("cancelled", refund_and_release),
# ("shipped", "cancel"): ("returning", start_return),
# }
#
# Reach for classes when each state carries data or several rich operations;
# reach for the table when the machine is mostly bookkeeping.
#include <memory>
#include <variant>
struct Pending {};
struct Paid { Receipt receipt; };
struct Shipped { TrackingId tracking; };
struct Delivered {};
struct Cancelled {};
using OrderState = std::variant<Pending, Paid, Shipped, Delivered, Cancelled>;
class Order {
public:
void cancel() {
state_ = std::visit(Cancel{*this}, state_);
}
private:
struct Cancel {
Order& order;
OrderState operator()(Pending) const { inventory::release(order); return Cancelled{}; }
OrderState operator()(const Paid& p) const { payments::refund(p.receipt); return Cancelled{}; }
OrderState operator()(const Shipped& s) const { returns::start(s.tracking); return Shipped{s}; }
OrderState operator()(Delivered) const { throw IllegalTransition{"cancel"}; }
OrderState operator()(Cancelled) const { throw IllegalTransition{"cancel"}; }
};
OrderState state_{Pending{}};
};
// std::variant + std::visit gives C++ something the classic virtual version
// can't: the compiler REFUSES to build if you add a state and forget an
// overload. Each state can also carry its own data — Paid has a receipt,
// Shipped has a tracking id — which a plain enum cannot express at all.
public abstract record OrderState
{
public virtual OrderState Pay(Order order) => throw new IllegalTransition(nameof(Pay));
public virtual OrderState Cancel(Order order) => throw new IllegalTransition(nameof(Cancel));
}
public sealed record Pending : OrderState
{
public override OrderState Pay(Order order) => new Paid(Payments.Charge(order));
public override OrderState Cancel(Order order) { Inventory.Release(order); return new Cancelled(); }
}
public sealed record Paid(Receipt Receipt) : OrderState
{
public override OrderState Cancel(Order order)
{
Payments.Refund(Receipt);
Inventory.Release(order);
return new Cancelled();
}
}
public sealed class Order
{
private OrderState _state = new Pending();
public void Pay() => _state = _state.Pay(this);
public void Cancel() => _state = _state.Cancel(this);
}
// Returning the next state instead of mutating the context makes transitions
// pure and trivially testable: Assert.IsType<Cancelled>(new Paid(r).Cancel(o)).
//
// For anything with more than a handful of states, a library (Stateless) buys
// you guard clauses, entry/exit actions and a DOT graph of the machine — and a
// picture of your state machine is worth a great deal in a design review.
// A plain object of state handlers. No classes needed, and the whole machine is
// visible in one screen — which is itself a feature.
const states = {
pending: {
pay: order => { payments.charge(order); return 'paid' },
cancel: order => { inventory.release(order); return 'cancelled' },
},
paid: {
ship: order => { warehouse.dispatch(order); return 'shipped' },
cancel: order => {
payments.refund(order.receipt)
inventory.release(order)
return 'cancelled'
},
},
shipped: {
cancel: order => { returns.start(order); return 'returning' },
},
delivered: {}, // nothing is legal here
}
export const send = (order, event) => {
const handler = states[order.status]?.[event]
if (!handler) throw new IllegalTransition(order.status, event)
order.status = handler(order)
return order
}
// XState is the serious version of this for front-end work: statecharts with
// guards, nested states, entry/exit actions and a visualiser. Once a machine
// has more than about six states, a diagram you can look at beats any amount of
// carefully-named code.
package orders
// Go has no inheritance, so a State is an interface and each state is a type.
// Returning the next state (rather than mutating) keeps every transition pure.
type State interface {
Cancel(*Order) (State, error)
Ship(*Order) (State, error)
}
type Paid struct{ Receipt Receipt }
func (p Paid) Ship(o *Order) (State, error) {
if err := warehouse.Dispatch(o); err != nil {
return p, err // stay put on failure
}
return Shipped{}, nil
}
func (p Paid) Cancel(o *Order) (State, error) {
if err := payments.Refund(p.Receipt); err != nil {
return p, err
}
inventory.Release(o)
return Cancelled{}, nil
}
type Delivered struct{}
func (Delivered) Cancel(*Order) (State, error) { return Delivered{}, ErrIllegalTransition }
// Note the error return doing work no exception-based version gets for free:
// a failed transition returns the CURRENT state, so a payment failure can't
// leave the order in a half-moved position.
//
// Go's other famous state machine is Rob Pike's lexer: a state is a FUNCTION
// that returns the next state function — `type stateFn func(*lexer) stateFn` —
// and the whole scanner is a loop calling `state = state(l)`.
type OrderState =
| { status: 'pending' }
| { status: 'paid'; receipt: Receipt }
| { status: 'shipped'; tracking: TrackingId }
| { status: 'delivered' }
| { status: 'cancelled'; reason: string }
// Each state carries exactly the data that state has — `tracking` doesn't
// exist on a pending order, so no code can read it by accident.
export function cancel(state: OrderState, order: Order): OrderState {
switch (state.status) {
case 'pending':
inventory.release(order)
return { status: 'cancelled', reason: 'user' }
case 'paid':
payments.refund(state.receipt) // typed: receipt exists here
inventory.release(order)
return { status: 'cancelled', reason: 'user' }
case 'shipped':
returns.start(state.tracking) // typed: tracking exists here
return state
case 'delivered':
case 'cancelled':
throw new IllegalTransition('cancel', state.status)
}
}
// This is the strongest version of the pattern available in any of the seven
// languages. Add 'partiallyShipped' to the union and EVERY function that
// switches on status becomes a compile error until you handle it — the exact
// bug that the six-switches design ships to production silently.
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
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.
Define the state interface with every operation the context supports.
Make illegal the default: a base class or default method that throws, so each state opts in to what it permits.
Write one class per state, holding only the data that state actually has.
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.
Prefer returning the next state to mutating the context — pure transitions are trivially testable and can't leave a half-moved object behind.
Make illegal transitions loud: an exception or an error return, never a silent no-op.
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…
Use
Because
Change behaviour as an object's situation changes, over time
State
States swap themselves and know the legal transitions.
Let the caller choose an interchangeable algorithm
Stateless 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?
The diagrams really are the same, which is why this is such a common interview question. The difference is in the dynamics. A Strategy is chosen by the client from outside, usually once, and the strategies are mutually ignorant — QuickSort has never heard of MergeSort. A State is swapped from inside, as a consequence of what happens to the object, and states typically name their successors, so the set of them encodes a machine with legal and illegal transitions. Intent follows from that: Strategy varies how something is done; State varies what the object is allowed to do right now.
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).
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.