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.

For objects arranged in a tree, see the Composite and Visitor patterns, which walk a file tree with one call and add operations as visitor classes.

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.

On the mind map

Open the Software Design mind mapSoftware DesignPrinciples: keep each change localPrinciplesDesign patterns: named fixes for common designsDesign patternsArchitecture: the shape of a whole appArchitectureEnterprise app patterns: logic, SQL, locks, money (PoEAA)Enterprise app patternsGit storage: content named by its hashGit storageGit branches & merging: pointers to commits, then mergeGit branches & mergingTesting: check code in isolationTestingStrategy and FactoryStrategy and Factorypick behaviour by name, swap at runtimeState patternstate objects replace a big switchCommand and undoCommand and undoeach action is an object with undo()Decorator, Proxy, AdapterDecorator, Proxy, Adapterwrappers that share one interfaceObserver vs pub-subObserver vs pub-subobserver calls directly; pub-sub via brokerBuilder and SingletonBuilder and SingletonBuild step by step vs one shared instanceComposite and VisitorComposite and VisitorTreat a tree uniformly; add ops outside itChain of responsibilityChain of responsibilityEach handler acts or passes the request onOpen the Software Design mind mapSoftware Designnamed fixes for common designsDesign patternsStrategy and FactoryStrategy and Factorypick behaviour by name, swap at runtimeState patternstate objects replace a big switchCommand and undoCommand and undoeach action is an object with undo()Decorator, Proxy, AdapterDecorator, Proxy, Adapterwrappers that share one interfaceObserver vs pub-subObserver vs pub-subobserver calls directly; pub-sub via brokerBuilder and SingletonBuilder and SingletonBuild step by step vs one shared instanceComposite and VisitorComposite and VisitorTreat a tree uniformly; add ops outside itChain of responsibilityChain of responsibilityEach handler acts or passes the request on
This page on the Software Design mind map · 28 pages in 7 branches · open it to see the whole map and test yourself in recall mode.