State Pattern
Allow an object to alter its behavior when its internal state changes, appearing to change its class at runtime.
Overview
Most objects in a system go through phases. An order is placed, then paid, then shipped, then delivered. A vending machine waits for a coin, accepts a selection, and dispenses a product. At each phase, the same method call — "cancel the order", "press a button" — should do something completely different.
The State pattern is a behavioral design pattern that tackles this problem head-on. Instead of cramming every phase's logic into a single class full of if/switch statements, you extract each state into its own object. The context delegates its behavior to whichever state object it currently holds.
The result is code where adding a new state means adding a new class, not hunting through nested conditionals. Each phase's rules live in one place and are easy to read in isolation.
Problem & Motivation
Imagine you are modeling an order processing workflow. An order can be in one of four states: Pending, Paid, Shipped, and Delivered. Each state constrains what actions are valid:
- A Pending order can be paid or cancelled.
- A Paid order can be shipped or refunded, but not paid again.
- A Shipped order can only be marked as delivered.
- A Delivered order is terminal — no transitions are allowed.
Without the State pattern the Order class accumulates a status field and every method starts with a switch block:
ship() {
if (status === "PENDING") throw new Error("Not paid yet")
if (status === "SHIPPED") throw new Error("Already shipped")
if (status === "DELIVERED") throw new Error("Already delivered")
status = "SHIPPED"
}Multiply this by five or six methods and four or five states and you have a maintenance nightmare. Adding a "Disputed" state means touching every single method. The State pattern replaces this with a clean delegation model where each state class implements only its own rules.
Class Diagram
Loading diagram...
The structural elements to notice:
- Context (
Order) holds a reference to the currentOrderStateobject and forwards all state-dependent calls to it. - State interface (
OrderState) declares an identical method signature for each action every state must handle. - Concrete states (
PendingState,PaidState,ShippedState) implement only their own valid transitions and throw errors for invalid ones. - State transitions happen by calling
context.setState(new NextState())from inside a concrete state method — the state controls its own transitions.
Implementation
All four implementations model the same order workflow: Pending → Paid → Shipped → Delivered, with cancellation allowed only from Pending and Paid.
interface OrderState {
pay(order: Order): void;
ship(order: Order): void;
deliver(order: Order): void;
cancel(order: Order): void;
}
class Order {
private state: OrderState;
constructor() {
this.state = new PendingState();
}
setState(state: OrderState): void {
this.state = state;
}
pay(): void { this.state.pay(this); }
ship(): void { this.state.ship(this); }
deliver(): void { this.state.deliver(this); }
cancel(): void { this.state.cancel(this); }
}
class PendingState implements OrderState {
pay(order: Order): void {
console.log("Payment received. Order is now Paid.");
order.setState(new PaidState());
}
ship(order: Order): void {
throw new Error("Cannot ship an unpaid order.");
}
deliver(order: Order): void {
throw new Error("Cannot deliver an unpaid order.");
}
cancel(order: Order): void {
console.log("Order cancelled from Pending.");
}
}
class PaidState implements OrderState {
pay(order: Order): void {
throw new Error("Order is already paid.");
}
ship(order: Order): void {
console.log("Order shipped. In transit.");
order.setState(new ShippedState());
}
deliver(order: Order): void {
throw new Error("Cannot deliver before shipping.");
}
cancel(order: Order): void {
console.log("Payment refunded. Order cancelled from Paid.");
}
}
class ShippedState implements OrderState {
pay(order: Order): void {
throw new Error("Order is already paid.");
}
ship(order: Order): void {
throw new Error("Order is already shipped.");
}
deliver(order: Order): void {
console.log("Order delivered successfully.");
}
cancel(order: Order): void {
throw new Error("Cannot cancel a shipped order.");
}
}
// Usage
const order = new Order();
order.pay(); // Payment received. Order is now Paid.
order.ship(); // Order shipped. In transit.
order.deliver(); // Order delivered successfully.Who Owns the Transition?
In the examples above, concrete state objects call order.setState(new NextState()) to drive transitions. This is the most common approach because each state knows its own valid successors. An alternative is to let the context itself decide — state methods return the next state and the context applies it. Choose self-transitioning states when state objects have no other reason to hold a reference to the context; choose context-driven transitions when transition logic depends on data the context holds.
Real-World Examples
E-commerce order lifecycles — Platforms like Shopify and WooCommerce model order status as a state machine. Each status (pending payment, processing, on-hold, completed, refunded) enables or disables different admin actions. The State pattern ensures that triggering a refund on a pending order is impossible at the type level, not just at the UI level.
TCP connection management — A TCP socket moves through states: CLOSED, LISTEN, SYN_SENT, ESTABLISHED, FIN_WAIT, TIME_WAIT, and others. Each state responds differently to the same events (receive SYN, receive FIN, send data). The classic GoF book uses TCP as its primary State pattern example precisely because the rules are well-defined and the conditional alternative would be unmanageable.
UI component modes — A text editor's toolbar button for "Bold" behaves differently depending on whether text is selected, no text is selected, or the cursor is in a read-only region. Rich text editors like ProseMirror model editor state explicitly, allowing plugins to query and respond to state changes without reading scattered flags across the application.
Vending machines — A vending machine waits for coins (IdleState), holds a pending selection (HasCoinState), dispenses a product (DispensingState), and handles out-of-stock situations (OutOfStockState). Each physical button press delegates to the current state object, which either handles it or prints an error message.
Pros and Cons
Advantages
Eliminates conditionals — state-specific behavior is distributed across state classes instead of concentrated in switch/if chains. Each class is short and focused.
Open/Closed Principle — adding a new state means adding a new class and wiring it into one or two existing transitions. No existing state class needs to change.
Explicit state machine — the set of states, their transitions, and their rules are all visible in code. A new team member can read the state classes and understand the full lifecycle without decoding nested logic.
Testability — each state class can be unit tested independently by constructing it directly and calling its methods with a mock context.
Disadvantages
Class proliferation — every state becomes a class. A workflow with ten states and six actions produces ten classes plus a context. This is the right trade-off for complex workflows but over-engineering for simple ones.
Spread logic — understanding a full transition requires jumping between multiple files. An IDE with "find usages" helps, but the flow is less linear than a single switch block.
Shared state is tricky — if state objects need to read or write data on the context, you must either pass the context to every method (as shown above) or store data redundantly in the state object. Both approaches work but require discipline.
Transition visibility — because each state decides its own successor, the complete set of valid transitions is not in one place. A state machine diagram (or a dedicated transition table) is helpful documentation to add alongside the code.
When to Use / When to Avoid
Use State when:
- An object's behavior differs substantially across two or more states and those differences are expected to grow or change independently.
- You have methods that contain long switch or if-else chains keyed on a status field.
- You want to enforce valid transitions at the code level rather than relying on runtime checks scattered across the codebase.
- You are modeling a real-world process with a known lifecycle (orders, tickets, connections, documents).
Avoid State when:
- The object has only two states and the logic difference is a single condition. A boolean flag and one
ifis clearer. - State transitions are purely data-driven and loaded from configuration (a database-backed workflow engine). Encoding states as classes adds no benefit when the set of states is dynamic.
- The behavior variation is minor — prefer a simple strategy swap or a guard clause instead.
- You need a formal state machine with history, parallel states, or hierarchical substates. In those cases reach for a dedicated state machine library (XState in JavaScript, transitions in Python, Stateless in .NET) rather than the raw pattern.
Related Patterns
Strategy — the closest sibling of State. Both patterns delegate behavior to a family of interchangeable objects. The difference is intent: Strategy selects an algorithm based on a caller's choice and does not change during an object's lifetime. State changes automatically in response to events and the context transitions between states. A common rule of thumb: if the object swaps its behavior itself, it is State; if an external caller swaps it, it is Strategy.
Command — often used together with State. Commands represent actions requested by the user; the State pattern determines whether and how those commands are handled given the current state. This combination appears frequently in workflow engines and game input systems.
Flyweight — state objects are stateless by design (they hold no instance data specific to one context). When many contexts share the same state, a single state object can be shared across all of them using the Flyweight pattern. This is a common optimization in high-throughput systems where millions of entities cycle through the same small set of states.
Observer — state transitions are natural events. Coupling State with Observer lets subscribers react to transitions (audit logging, analytics, side-effects) without modifying the state classes themselves.
Design Tip
Before implementing State, draw the state machine on paper or in a tool like Mermaid. Define every state, every valid transition, and what triggers each transition. If the diagram fits on a napkin, the pattern is worth it. If it requires a whiteboard and a legend, consider whether a workflow engine or a dedicated state machine library would serve you better — they offer debugging tools, visualization, and persistence that hand-rolled State cannot match.