State Pattern
Concept
State lets an object change its behavior when its internal state changes, by extracting each state's behavior into its own class and delegating to whichever one is currently active, rather than encoding "what to do in each state" as conditional branches inside the object itself. The object being modeled (the "context") holds a reference to a current-state object implementing a shared interface; every method the context exposes just forwards the call to the current state, and a state transition is nothing more than reassigning that reference to a different state object. Each state class implements only the behavior valid for that state, and — critically — each state object decides its own transitions: an Order in a Shipped state knows it can only move to Delivered or Returned, and encodes that directly rather than leaving it to a central if/elif chain elsewhere to get right.
This is the machinery behind every explicit state machine in a real system: an order-processing pipeline (Pending → Paid → Shipped → Delivered), a TCP connection's state diagram, a media player (Playing/Paused/Stopped), a workflow/approval engine, or a game character's behavior modes (Idle/Attacking/Stunned). Libraries like Python's transitions, XState (JS/TS), or Spring's StateMachine formalize the same idea with declarative transition tables instead of hand-rolled state classes, but the underlying model — behavior and legal transitions live with the state, not scattered through conditionals — is identical.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
Conditional/flag-based (a status field checked by if/switch in every method) |
No extra classes; fine for 2-3 states with simple, stable behavior | Every new state adds a branch to every method that checks status — behavior for one state is smeared across the whole class, and it's easy to leave a branch unhandled when a new state is added |
| State pattern (behavior + legal transitions live in per-state classes) | Adding a state means adding one class, not touching every existing method; each state's logic and valid transitions are colocated and reviewable in isolation | More classes/files for a simple state set; overkill when there are only two states or behavior barely differs between them |
| Declarative state machine (transition table + library, e.g. XState) | Transitions and guards are explicit, visualizable, and often testable/verifiable directly from the table, catching illegal-transition bugs before runtime | Adds a library dependency and a table-reading indirection; less natural fit when a state's behavior is genuinely complex code rather than a simple transition |
The State pattern and a declarative state machine library solve the same problem at different levels of formality — hand-rolled State classes give full code flexibility per state, a transition-table library gives a single reviewable source of truth for what transitions are even legal, catching an invalid transition as a config error rather than a runtime bug.
When to use / when not to
- Use when an object's behavior genuinely varies by a well-defined, named state, and that state has more than a couple of values, or the state's valid transitions are a real invariant worth protecting (you can't ship an order that hasn't been paid).
- Especially valuable when new states are added over the system's life — State keeps that change local to one new class instead of hunting down and updating every conditional across the codebase.
- Don't reach for it for a simple boolean flag (
isActive) with one or two trivial behavior differences — a plain conditional is more readable and the pattern's class-per-state ceremony isn't buying anything. - Skip it when "state" is really just data, not behavior — if nothing about how the object behaves changes per state, you likely just need an enum field, not a State pattern.
Common pitfall
Letting transition logic leak outside the state classes — some other part of the system starts deciding "if we're in Shipped, move to Delivered" instead of the Shipped state object making that call itself. Once transition rules live in two places (inside state classes and in whatever external code also branches on the current state), they drift: someone adds a new state, updates the state classes, but misses an external if that still assumes the old fixed set of states, and now an object can end up transitioning somewhere the state machine was never designed to allow. The pattern only pays off if every legal transition is owned by exactly one place — the current state itself.
Engineering Lens
The strong signal that a system needed State and didn't get it is a bug report that reads "order X ended up Delivered without ever being Shipped" — that's not a logic bug in one function, it's a missing invariant, because nothing in the codebase's structure made that transition impossible to write by accident. State earns its complexity precisely by making illegal transitions a compile-time or structural non-option rather than a runtime hope that every call site remembered to check the right condition. In a review, the useful question isn't "did you use the State pattern" — it's "show me the one place in the code where it's impossible to transition from Shipped directly to Delivered without passing through the states in between," because if that place doesn't exist, the invariant isn't actually enforced, it's just usually followed.
Related
- Command Pattern — both encapsulate behavior as objects, but Command wraps a single request/action while State wraps everything an object can do while in a given mode
- Observer Pattern — often paired with State: interested parties subscribe to notifications when a state transition happens