AcademyDesign Patterns
State pattern
Letting an object swap what it can do next, decided from inside itself, the state class and transition table it costs, and two states that disagree who owns a transition.
6 min readUpdated 3 exercises
State looks like Strategy from the outside, the same interchangeable-implementation shape, but is chosen by the object itself, and changes over time as the object reacts to what happens to it.
State looks like Strategy wearing a different name: the same shape, an interface with interchangeable implementations a class delegates to. But the intent flips. A Strategy is picked once, from outside, and stays fixed. A State is picked by the object itself, and changes over time as the object reacts to what happens to it, with each state deciding both what it can do and which state comes next.
The problem it solves
A phone can be idle or in a call. Picking up while already in a call makes no sense, and neither does hanging up while idle. A first version uses a status field and checks it everywhere:
pickUp() {
if (this.status === 'idle') this.status = 'active';
else if (this.status === 'active') throw new Error('already active');
}- Every method repeats the same checks. With five states and six actions, that is thirty combinations spread across the class.
- Adding a state means editing every method.
- Illegal transitions are easy to miss. Nothing forces you to decide what happens for each pair.
The fix is one small class per state. Each state defines what every action does while in that state, and which state comes next.
Structure
Three parts:
- A state interface: the actions the context supports.
- Concrete states (
IdleState,ActiveState): each implements the actions for that state, and returns the next state. - The context (
Phone): holds the current state and delegates every action to it.
class IdleState {
pickUp() { return new ActiveState(); }
hangUp() { throw new Error('already idle'); }
}
class ActiveState {
pickUp() { throw new Error('already active'); }
hangUp() { return new IdleState(); }
}
class Phone {
constructor() { this.state = new IdleState(); }
pickUp() { this.state = this.state.pickUp(); }
hangUp() { this.state = this.state.hangUp(); }
}
// const phone = new Phone();
// phone.pickUp(); → state becomes ActiveState
// phone.hangUp(); → state becomes IdleState again
// phone.hangUp(); → throws 'already idle'Phone has no if on a status. Asking an idle phone to hang up throws, because IdleState.hangUp says so. The rules live with the state they belong to.
Where it is used in real systems
| Use case | The states |
|---|---|
| Order lifecycle | Pending, paid, shipped, delivered, cancelled, refunded. |
| Subscription status | Trial, active, past due, cancelled. |
| UI components | Idle, loading, success, error for a request. |
| Media players | Stopped, playing, paused, buffering. |
| Documents | Draft, in review, approved, published. |
| Connections | Connecting, open, closing, closed. |
| Game characters | Standing, jumping, ducking, attacking. |
| Authentication flows | Signed out, verifying, signed in. |
If you have a status field and several methods that all start with if (status === ...), you are looking at a State machine written by hand.
States you already use
- Promises. Pending, fulfilled or rejected, and the transitions are one-way.
- Fetching in React Query or SWR.
isLoading,isSuccessandisError, each rendering something different. - WebSocket `readyState`. Connecting, open, closing, closed.
- Lemon Squeezy subscription status.
on_trial,active,past_due,cancelled, each allowing different actions. - XState models whole applications as state machines.
- Git. A rebase in progress, a merge conflict and a clean tree each allow different commands.
State classes or a table
You do not always need a class per state. For many cases a transition table is smaller and easier to read:
type Status = 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
const TRANSITIONS: Record<Status, readonly Status[]> = {
pending: ['paid', 'cancelled'],
paid: ['shipped', 'cancelled'],
shipped: ['delivered'],
delivered: [],
cancelled: [],
};
function canMove(from: Status, to: Status): boolean {
return TRANSITIONS[from].includes(to);
}Use a table when states differ only in which transitions are allowed. Use State classes when each state also has different behavior, not just different permissions.
State or Strategy
| Strategy | State | |
|---|---|---|
| Who chooses | The client, from outside | The object itself, in response to events |
| Changes over time | Rarely | Constantly |
| Implementations know each other | No | Yes, each knows which state comes next |
| Purpose | Pick an algorithm | Model a lifecycle |
When not to use it
- Two or three states and simple rules. A status field and one check may be clearer.
- States differ only in permissions. A transition table is smaller.
- Many states with mostly shared behavior. One class per state means a lot of duplication. Use a table, or a library like XState.
- State stored in a database. The persisted value is a string. You still need a mapping from that string to the class when you load it.
What interviewers ask
- "What is the State pattern?" An object changes its behavior when its internal state changes, by delegating to a state object.
- "State versus Strategy?" Same structure, different intent. State changes itself over time. Strategy is picked by the client.
- "How does it remove `if/else`?" Each state class holds the behavior for that state, so the context has no branching.
- "Where would you use it in a backend?" Order or subscription lifecycles, with valid transitions enforced.
- "How do you handle invalid transitions?" The state throws or returns an error, instead of the context checking.
The mistake to avoid
Letting the state be changed from outside. If any code can write phone.state = new ActiveState(), the transition rules are no longer enforced. Keep the current state private, and let only state objects decide what comes next.
The mechanism above, an object delegating to a replaceable helper that can replace itself, is the same in JavaScript, Python or Java. Only the syntax changes.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- StateProImplement a state machine where each state decides its own legal transitions — the self-transitioning object this page builds, made runnable.ChallengeSolve · MediumChallengeSolve · Medium
- Design Patterns · Behavioral PatternsTen quick questions across Strategy, State, Observer, Command and the rest of the behavioral family, to check the mental model before this one.QuizTake · HardQuizTake · Hard
- Design Patterns · Behavioral Patterns — Series 3ProA second, tougher pass on the same behavioral family, for once the first set feels easy.QuizTake · HardQuizTake · Hard
Read next
Strategy pattern
Swapping an algorithm without touching the class that uses it, the interface every strategy must implement even for a one-off, and reaching for it before a second case exists.
6 min
Template Method pattern
Fixing the step order of an algorithm in a base class while subclasses fill in the rest, the subclassing it costs to change one step, and a base class open to rewrites.
6 min