State Pattern
An object's behaviour changes with its internal state — represented as a class hierarchy. The state-machine pattern in disguise.
What it is#
The State pattern lets an object alter its behaviour when its internal state changes — the object will appear to change its class. The Gang of Four named the two roles Context (the object whose behaviour depends on state) and State (the polymorphic delegate that handles operations for one specific state).
Structurally, State looks identical to Strategy. The difference is intent: a Strategy is chosen from outside and stays put; a State swaps itself in response to events the context receives. A TCPConnection does not get told “now you’re Established” by its caller — it transitions there because a SYN+ACK arrived. The state object owns the transition logic.
Class structure#
┌────────────────────┐ ┌──────────────────────┐ │ Context │◇───────►│ State │ ├────────────────────┤ 1 ├──────────────────────┤ │ state │ │ handleA(ctx) │ │ request(...) │ │ handleB(ctx) │ │ setState(s) │ │ handleC(ctx) │ └────────────────────┘ └──────────┬───────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌──────────┴───┐ ┌─────────┴────┐ ┌────────┴─────┐ │ StateA │ │ StateB │ │ StateC │ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ handleA(c) │ │ handleA(c) │ │ handleA(c) │ │ handleB(c) │ │ handleB(c) │ │ handleB(c) │ │ handleC(c) │ │ handleC(c) │ │ handleC(c) │ └──────────────┘ └──────────────┘ └──────────────┘The state classes call context.setState(...) to transition. Each state implements all operations of the interface — even ones it forbids (which typically throw or no-op).
When to use it#
Reach for State when the answer to every one of these is yes:
- The object has distinct named modes of operation (Pending → Approved → Shipped → Delivered).
- Multiple operations behave differently per state — not just one. (
ifon a single method does not earn a class hierarchy.) - The transition graph is non-trivial — some transitions are forbidden, some self-loop, some fan out.
- The set of states is stable — adding a new state happens occasionally, not on every commit.
Common scenarios:
- Network protocols — TCP’s Listen / SYN-Received / Established / Fin-Wait state machine.
- Document workflows — Draft / In Review / Approved / Published / Archived, where what you can do differs per state.
- Order lifecycle — Pending → Confirmed → Packed → Shipped → Delivered, with refund/cancel allowed only in some states.
- UI components — a button’s Idle / Hover / Pressed / Disabled states.
- Game entities — a character’s Idle / Walking / Running / Jumping / Falling with state-specific input handling.
When not to use it:
- When you have one method that branches on a state field.
switch (state)is two lines; State pattern is six classes. Wait until the branching repeats across three or more methods. - When the states are really just flags that combine freely (
isAuthenticated && isPaid && isVerified). Those are boolean fields, not mutually-exclusive states. - When the transition graph is a single forward sequence with no choices. A counter or a phase index is simpler.
How it works#
The textbook example is a TCPConnection with three states: Listening, Established, Closed. Each handles open, close, and acknowledge differently.
public interface TcpState { void open(TcpConnection ctx); void close(TcpConnection ctx); void acknowledge(TcpConnection ctx); String name();}
public final class Listening implements TcpState { public static final Listening INSTANCE = new Listening(); private Listening() {} @Override public void open(TcpConnection ctx) { System.out.println("SYN received — moving to Established"); ctx.setState(Established.INSTANCE); } @Override public void close(TcpConnection ctx) { System.out.println("Closing from Listening"); ctx.setState(Closed.INSTANCE); } @Override public void acknowledge(TcpConnection ctx) { throw new IllegalStateException("cannot ack while Listening"); } @Override public String name() { return "Listening"; }}
public final class Established implements TcpState { public static final Established INSTANCE = new Established(); private Established() {} @Override public void open(TcpConnection ctx) { // Already open — idempotent. } @Override public void close(TcpConnection ctx) { System.out.println("FIN sent — moving to Closed"); ctx.setState(Closed.INSTANCE); } @Override public void acknowledge(TcpConnection ctx) { System.out.println("ACK delivered"); } @Override public String name() { return "Established"; }}
public final class Closed implements TcpState { public static final Closed INSTANCE = new Closed(); private Closed() {} @Override public void open(TcpConnection ctx) { throw new IllegalStateException("cannot reopen a Closed connection"); } @Override public void close(TcpConnection ctx) { // Already closed — idempotent. } @Override public void acknowledge(TcpConnection ctx) { throw new IllegalStateException("cannot ack a Closed connection"); } @Override public String name() { return "Closed"; }}
public final class TcpConnection { private TcpState state = Listening.INSTANCE;
void setState(TcpState s) { this.state = s; } public String currentState() { return state.name(); }
public void open() { state.open(this); } public void close() { state.close(this); } public void acknowledge() { state.acknowledge(this); }}
// Wiring:// TcpConnection c = new TcpConnection();// c.currentState(); // Listening// c.open(); // -> Established// c.acknowledge(); // ACK delivered// c.close(); // -> Closed// c.open(); // IllegalStateExceptionFive details earn their keep:
- States are singletons. They hold no per-context data, so one instance suffices. The context passes itself in on every call, which is how the state can mutate
ctx. setStateis package-private. Only states transition the context; callers cannot force a transition by reaching in. This is the encapsulation the pattern is selling.- Forbidden operations throw.
acknowledgeon aClosedconnection is a programmer error, not a runtime branch — throw and fail loud. - Idempotent operations no-op.
closeon aClosedconnection should not throw. Idempotence is a design choice per operation; the pattern lets you decide per state. - The context exposes
currentState(), not the state object itself. Callers can read the mode for logging and tests without coupling to the state classes.
Variants#
| Variant | Mechanism | When it fits |
|---|---|---|
| Stateful state objects | State objects carry their own per-instance data (history, timers, counters). | Each state needs memory the context should not hold. Lose the singleton. |
| Enum-based state | States are enum constants; the enum implements the state interface. | A small, fixed set of states with simple transition logic. Java’s enum does this elegantly. |
| Table-driven | Transitions live in a Map<(State, Event), State> rather than inside state classes. | Many states, many events, and the transition graph is the part most likely to change. State classes hold behavior; the table holds shape. |
| Hierarchical state machines | States nest — Active contains Idle and Working, both inheriting Active’s common behavior. | UI components, game AI, anywhere parent-state behavior is shared across child states. |
The vending-machine example illustrates the table-driven variant well: the states (Idle, HasCoin, Dispensing, OutOfStock) and events (insertCoin, selectItem, dispense, restock) cross at a small number of valid transitions; the rest are errors. A 4×4 table is more readable than four state classes with three forbidden methods each.
Example systems#
- TCP/IP stack — the canonical example. Every connection is a state machine; protocol books draw the diagram on page one.
- Order management systems — Pending / Confirmed / Packed / Shipped / Delivered / Cancelled / Refunded, where refund eligibility, cancellation rules, and customer-visible status all depend on state.
- Document workflows in CMSes — Draft / In Review / Approved / Published / Archived, with permission rules per state.
- Vending machines, ATMs, parking gates — physical state machines that translate directly into the pattern. See
parking-lot-designandatm-system-designin this workbook. - Game character controllers — Unity’s animator state machines, Unreal’s behavior trees, custom engines all model entity behavior as states with transitions.
- Build pipelines — Queued / Running / Succeeded / Failed / Cancelled with retry rules per terminal state.
Trade-offs#
What you gain:
- State-specific behavior in one place. All the “what does
acknowledgemean while Established” lives inEstablished. No grep across the codebase forif (state == ESTABLISHED). - Transitions are explicit and audit-able.
ctx.setState(...)calls are the entire transition surface. Logging or persisting them is one cross-cut. - Open for new states. Adding
TimeWaitis a new class; the existing states do not change. (Compare to a switch statement that must grow a case in every method.) - Forbidden operations fail at the boundary.
cannot ack while Listeningis thrown byListening.acknowledgedirectly — the context does not need a guard.
What you pay:
- Class explosion for trivial machines. Three states with one method each is three classes. The switch version is twelve lines. Pick by signal-to-noise.
- Transition logic is scattered. Each state knows what it transitions to. Reading the full graph requires visiting every state class — the table-driven variant solves this when the graph is the priority.
- Sharing data between states is awkward. If
Establishedneeds to know how long agoListeningended, the context must hold it. Singletons cannot. - The hierarchy is a refactor magnet. When a sixth method joins the interface, every existing state must implement it. The interface is your coupling point.
State pattern: polymorphic dispatch
Each state is a class. Adding a state is a new file. Behaviour for op while in S1 lives in S1.op. Operations that span all states (a “what state am I in?” log) require a method on every state.
Switch on enum: data-driven dispatch
States are enum constants. Adding a state is a new constant plus a case in each method. All variants of op live in op — one method, one switch. Cross-cutting operations are cheap; per-state-with-many-methods is verbose.
The honest rule: switch-on-enum wins when there are few methods and many states; State pattern wins when there are many methods and few states. Each new state in the switch model adds a constant; each new state in the pattern model adds a class. The trade is exactly that.
Related patterns#
- Strategy Pattern — structurally identical; intent differs. Strategy is chosen from outside and stays; State swaps itself from inside in response to events. If your “strategy” reassigns itself based on what just happened, it is really State.
- Observer Pattern — state transitions are an excellent event source. Subscribe observers to
Order.stateChangedand the audit log writes itself. - Command Pattern — Commands often cause state transitions; the State object can refuse a Command that is not valid in this state, giving you free validation.
- Open Closed Principle (OCP) — State is the OCP refactor for “we keep adding new modes that change every operation.”
- Polymorphism — State leans hard on subtype polymorphism. The context’s
state.open(this)call is a virtual dispatch that picks the right behavior. - Parking Lot, ATM System — both designs use State for the gate / session lifecycle.