The idea: let the state object answer

Some objects behave differently depending on their state. An order can be paid only when it is new, shipped only when it is paid, and cancelled only before it ships. The State pattern (Gamma, Helm, Johnson and Vlissides, 1994) puts each state in its own class. The object that has the state, the Context, keeps a reference to one State object and forwards every request to it.

Class diagram: Order (the Context) has a field state of type OrderState. Five classes implement OrderState: NewState, PaidState, ShippedState, DeliveredState and CancelledState. Order.ship() calls state.ship(this).
The Order holds one reference to an OrderState; each concrete state class decides what pay(), ship(), deliver() and cancel() do.

The canvas follows order #42 (40.00). On top, the object view shows the client, the Order with its state field, and one object per state class. Next to it, the state diagram shows which requests each state allows, with the current state in orange. Below, the code of Order and of the class that answers right now, and the same rules written as a switch on an enum.

Delegation and the swap

Every method of Order is one line: state.ship(this). The Order never checks its own state. The State object does the work and, if the request is allowed, calls o.setState(new ShippedState()). That swaps the reference, so the next call goes to a different object and behaves differently. To the client it looks as if the order changed its class.

Before and after order.ship(): before, the state field points at a PaidState object; PaidState.ship() calls o.setState(new ShippedState()); after, the field points at a ShippedState object.
One ship() call: PaidState handles it and points the Order at a ShippedState object.

A request can also have a side effect before the swap. NewState.pay() charges 40.00 through PaymentService, then moves to Paid. PaidState.cancel() refunds the 40.00, then moves to Cancelled (Demo 1: happy path, Demo 3: cancel after pay → refund).

Rejecting a request

A state that does not allow a request throws IllegalStateException. The reference stays where it was, and nothing else changes. NewState.ship() rejects with "not paid yet" (Demo 2: ship before pay is rejected). ShippedState.cancel() rejects with "already shipped" (Demo 4). Delivered and Cancelled are final states: their classes reject every request (Demo 5).

Statepay()ship()deliver()cancel()
Newcharge → Paid✗ not paid yet✗ not shipped yet→ Cancelled
Paid✗ already paid→ Shipped✗ not shipped yetrefund → Cancelled
Shipped✗ already paid✗ already shipped→ Delivered✗ already shipped
Delivered✗✗✗✗
Cancelled✗✗✗✗

State pattern vs a switch on an enum

The same rules fit in an enum Status and a switch (status) in every method. The canvas runs both versions on every request and they always agree. The difference is where the rules live.

switch on an enumState pattern
Rules of one statespread over 4 methodsin one class
Rules of one requestin one methodspread over 5 classes
Add a stateedit every switchadd one class, edit the classes that move to it
Add a requestadd one methodadd a method to every state class
Sizesmall: 1 classmore classes and an interface

A switch is fine for a few states with simple rules. The State pattern pays off when states have many requests, side effects of their own, or keep growing. It is an application of the open/closed principle (see SOLID Principles).

Where it shows up

A state machine is the model, the State pattern is one way to code it. The same New → Paid → Shipped shape appears in hardware as a finite state machine with a state register, in TCP connections (SYN_SENT, ESTABLISHED, CLOSE_WAIT), in drawing tools whose mouse clicks mean different things per tool, and in the phases of a Kubernetes pod. A circuit breaker is a small state machine too: CLOSED, OPEN, HALF_OPEN.

Variations

  • Who picks the next state. Here each state does (o.setState(…)). The Context can do it instead, from a transition table; then the states don't know each other.
  • New or shared state objects. These states hold no data, so one shared instance per class is enough (a singleton or an enum constant with a body). States with data (a retry count, a timer) are created fresh.
  • Strategy looks the same. Both forward to an object behind an interface. In Strategy the client picks the object and it rarely changes; in State the states swap it themselves as the object moves through its life.

What the page leaves out

A real order is stored in a database, so the state is saved as a column and the state object is rebuilt from it on each request. Two requests at the same time need a lock or an optimistic version check, or both could pass the same state. Real workflows also add timeouts, events to other services and an audit history. Frameworks such as Spring Statemachine generate the states and transitions from a configuration.