State Pattern
1. What the state pattern is
The state pattern gives each state of an object its own class, and lets the object delegate its behavior to whichever state object it currently holds. When the state changes, the object swaps that delegate, and its behavior changes with it. From the outside the object appears to change class.
It has three roles:
- State — an interface with one method per action the stateful object supports.
- Concrete state — one class per state. It implements the actions for that state, and it decides which state comes next.
- Context — the object the client actually uses. It holds a reference to a current state and forwards every action to it.
The shape is small: a context with a PlayerState field, and a handful of state classes behind it. The point is what moves out of the context. Behavior that used to branch on a state variable becomes ordinary method dispatch, and the branching disappears.
2. The problem: behavior that branches on state
Before the pattern, state-dependent behavior is a switch. Take an audio player with three states and three buttons. Without the pattern, the state is an enum and every button is a switch over it:
public class Player {
private enum State { STOPPED, PLAYING, PAUSED }
private State state = State.STOPPED;
public void play() {
switch (state) {
case STOPPED -> { startPlayback(); state = State.PLAYING; }
case PLAYING -> { /* already playing, ignore */ }
case PAUSED -> { resumePlayback(); state = State.PLAYING; }
}
}
public void pause() {
switch (state) {
case STOPPED -> { /* nothing to pause */ }
case PLAYING -> { pausePlayback(); state = State.PAUSED; }
case PAUSED -> { /* already paused, ignore */ }
}
}
public void stop() {
switch (state) {
case STOPPED -> { /* already stopped */ }
case PLAYING, PAUSED -> { stopPlayback(); state = State.STOPPED; }
}
}
}This works, and for three states and three actions it is arguably fine. It rots in two specific ways. First, the behavior of any one state is not in one place: what PAUSED does is spread across the PAUSED arm of play(), pause(), and stop(). To understand one state you read every method. Second, adding a state means editing every switch, and the compiler only helps if your switch is exhaustive over the enum and has no default. Miss one and a state silently does the wrong thing.
The switch also tangles two separate concerns in each arm: what the action does (resumePlayback()) and what state comes next (state = State.PLAYING). The state pattern splits the table the other way, by state instead of by action, so each state’s full behavior and all its transitions sit in one class.
3. Writing one in Java
The example is an audio player: the player itself is the context, and each button press is an action it forwards to its current state.
3.1. The State interface and the Player context
The interface has one method per action. Each method takes the context, so the state can call back into it and reassign it:
public interface PlayerState {
void play(Player player);
void pause(Player player);
void stop(Player player);
}The context holds a current state and forwards to it. It exposes the same three actions to the client, plus a package-private setState the states use to transition:
public class Player {
private PlayerState state = new StoppedState();
public void play() {
state.play(this);
}
public void pause() {
state.pause(this);
}
public void stop() {
state.stop(this);
}
void setState(PlayerState state) {
this.state = state;
}
}Every switch is gone. Player no longer knows what play() means in any given state, only that the current state does. That’s the trade. The context loses the branching and gains a layer of delegation.
3.2. The concrete states
Each state implements the interface for its own case. Here is StoppedState:
public class StoppedState implements PlayerState {
@Override
public void play(Player player) {
// start playback from the beginning
player.setState(new PlayingState());
}
@Override
public void pause(Player player) {
// nothing to pause when stopped
}
@Override
public void stop(Player player) {
// already stopped
}
}PlayingState and PausedState follow the same shape. PlayingState.pause() moves to PausedState; PausedState.play() moves back to PlayingState; both of their stop() methods move to StoppedState:
public class PausedState implements PlayerState {
@Override
public void play(Player player) {
// resume from the paused position
player.setState(new PlayingState());
}
@Override
public void pause(Player player) {
// already paused
}
@Override
public void stop(Player player) {
player.setState(new StoppedState());
}
}Now everything PausedState does is in PausedState. The “already paused, ignore” no-op that was buried in the PAUSED arm of the old pause() is right here, next to the transitions. Adding a fourth state, say BufferingState, is a new class plus edits only in the states that transition to or from it. No existing switch to hunt down, because there is no switch.
3.3. Who owns transitions
There are two places the next state can be decided: the states themselves, or the context. In §3.2 the states own it: StoppedState.play() is what knows that play-from-stopped leads to PlayingState. The alternative is a context that inspects the result of an action and picks the next state itself.
I went with state-owned transitions because it keeps the property that earned the pattern: one state, one class, no exceptions. If the context owned transitions it would need a rule per state per action, which is the switch from §2 coming back through the side door. The cost is that each state has a compile-time reference to its successors, so the states form a small dependency graph rather than a flat set. For an honest state machine, the transitions are the graph.
One refinement worth mentioning: the states here are cheap and stateless, so new PlayingState() on every transition is fine. If a state held data or were expensive to build, you would keep one shared instance per state (the states are flyweights at that point) and transition with player.setState(PlayingState.INSTANCE). Stateless states are also safe to share across multiple Player instances, which the per-transition new quietly gives up.
4. State vs strategy
The state pattern and the strategy pattern are the same diagram. Both have a context that holds an interface reference and delegates to it. The difference is entirely intent, and it shows up in two concrete places.
Who picks the implementation. A strategy is chosen once, from outside, usually at construction: you hand a Comparator to a sorter and it never changes. A state is reassigned at runtime, from inside, in response to the actions the context receives.
Whether the implementations know about each other. Strategies are independent and interchangeable; a quicksort strategy has no idea a mergesort strategy exists. Concrete states reference each other, because a state’s job includes naming its successors. That mutual awareness is the tell. If your “strategies” call setState, they are states.
So the question to ask of a candidate is whether the object transitions on its own. A Comparator does not become a different comparator because you used it. A PausedState becomes a PlayingState because you pressed play. If there are transitions it is state; if the implementation is picked and left alone, it is strategy.
5. When not to reach for it
The state pattern trades a switch for a class per state and a dependency graph between them. That is worth it when behavior genuinely varies across several states and the states transition between each other. Short of that, the enum-and-switch from §2 is the right call, not a thing to be ashamed of.
Concrete cases where the pattern is overkill:
- Two states. A boolean and an
ifis clearer than two classes and an interface. The pattern starts paying off around three or four states with non-trivial transitions. - No transitions. If the “state” is set once and never changes in response to actions, you do not have a state machine, you have a strategy. Use strategy (§4), or just a constructor argument.
- Behavior barely varies. If only one action out of five actually differs per state, putting all five on a state interface spreads four identical method bodies across every state class. Branch the one that varies and leave the rest alone.
The signal that you should reach for it: you are reading a switch over a state enum, it appears in more than two methods, and adding a state means finding and editing all of them. That’s the pattern asking to be used. Until then, a switch is a switch.