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.
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.
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).
| State | pay() | ship() | deliver() | cancel() |
|---|---|---|---|---|
| New | charge → Paid | ✗ not paid yet | ✗ not shipped yet | → Cancelled |
| Paid | ✗ already paid | → Shipped | ✗ not shipped yet | refund → 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 enum | State pattern | |
|---|---|---|
| Rules of one state | spread over 4 methods | in one class |
| Rules of one request | in one method | spread over 5 classes |
| Add a state | edit every switch | add one class, edit the classes that move to it |
| Add a request | add one method | add a method to every state class |
| Size | small: 1 class | more 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.