State Pattern
Allow an object to alter its behavior when its internal state changes. The GoF behavioral pattern for finite state machines.
Overview
Your Order class starts small: pay(), ship(), cancel(). Then each method grows a switch (status). Pay is only valid while pending, shipping only after payment, cancel except when shipped. Six months later the same switch is copy-pasted across every method, and adding a refunded state means auditing all of them.
The State pattern — one of the 11 behavioral patterns in the Gang of Four’s Design Patterns (Gamma et al., 1994) — is the classic fix: each state becomes its own class, the context delegates calls to the current state object, and the states themselves decide which transitions are legal. Adding a state means adding a class, not re-reading every conditional.
Two quick clarifications before diving in:
- This is the GoF object pattern: a way to model a finite state machine with classes. It’s not about UI state management: for React components and XState-style statecharts, see the State Machine UI Pattern.
- If your state machine has two or three states with trivial behavior, a plain
switchor enum is fine. The pattern earns its keep when transitions and per-state logic get complex.
When to Use
Use the State Pattern when:
- An object’s behavior depends on its state and must change at runtime: an
Orderthat can ship only after payment, aConnectionthat behaves differently open vs closed, a media player that ignoresplay()while stopped. - The same status check is copy-pasted across methods: the classic sign that a state machine is leaking conditionals everywhere.
- States have transitions with side effects: entry/exit actions like logging, notifications, or resource cleanup.
- New states appear regularly, and each should land as a new class without touching the existing ones (open/closed principle).
Don’t use it when the state machine is degenerate (two states, no transitions with behavior) or when a data-driven transition table (see Variants) already covers your needs. A switch on an enum is a legitimate choice; it rots when that same switch exists in five methods.
Problem
This is the shape the pattern removes: every method repeating the same state checks:
class Order:
def __init__(self):
self.status = "pending"
def pay(self):
if self.status == "pending":
print("Processing payment...")
self.status = "paid"
else:
print(f"Cannot pay in state {self.status}")
def ship(self):
if self.status == "paid":
print("Shipping order...")
self.status = "shipped"
else:
print(f"Cannot ship in state {self.status}")
# cancel() repeats the same if/elif ladder,
# and "refunded" would have to be added to all three.
The bug surface grows with every state: transitions are validated in three places, error messages drift apart, and refunded requires editing every method. The State pattern moves each if branch into a state class where it lives exactly once.
Solution
The same Order rebuilt around state objects: each state class owns its behavior and its outbound transitions:
Python
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def pay(self, order):
pass
@abstractmethod
def ship(self, order):
pass
@abstractmethod
def cancel(self, order):
pass
class PendingState(OrderState):
def pay(self, order):
print("Processing payment...")
order.transition_to(PaidState())
def ship(self, order):
print("Cannot ship: payment pending")
def cancel(self, order):
print("Order cancelled")
order.transition_to(CancelledState())
class PaidState(OrderState):
def pay(self, order):
print("Already paid")
def ship(self, order):
print("Shipping order...")
order.transition_to(ShippedState())
def cancel(self, order):
print("Issuing refund...")
order.transition_to(CancelledState())
class ShippedState(OrderState):
def pay(self, order):
print("Already paid")
def ship(self, order):
print("Already shipped")
def cancel(self, order):
print("Cannot cancel: already shipped")
class CancelledState(OrderState):
def pay(self, order):
print("Cannot pay: order cancelled")
def ship(self, order):
print("Cannot ship: order cancelled")
def cancel(self, order):
print("Already cancelled")
class Order:
def __init__(self):
self._state = PendingState()
def transition_to(self, state):
self._state = state
def pay(self):
self._state.pay(self)
def ship(self):
self._state.ship(self)
def cancel(self):
self._state.cancel(self)
# Usage
order = Order()
order.pay()
order.ship()
order.cancel() # Cannot cancel: already shipped
JavaScript
class Order {
constructor() { this.state = new PendingState(this); }
transitionTo(state) { this.state = state; }
pay() { this.state.pay(); }
ship() { this.state.ship(); }
cancel() { this.state.cancel(); }
}
class PendingState {
constructor(order) { this.order = order; }
pay() {
console.log("Processing payment...");
this.order.transitionTo(new PaidState(this.order));
}
ship() { console.log("Cannot ship: payment pending"); }
cancel() {
console.log("Order cancelled");
this.order.transitionTo(new CancelledState(this.order));
}
}
class PaidState {
constructor(order) { this.order = order; }
pay() { console.log("Already paid"); }
ship() {
console.log("Shipping order...");
this.order.transitionTo(new ShippedState(this.order));
}
cancel() {
console.log("Issuing refund...");
this.order.transitionTo(new CancelledState(this.order));
}
}
class ShippedState {
constructor(order) { this.order = order; }
pay() { console.log("Already paid"); }
ship() { console.log("Already shipped"); }
cancel() { console.log("Cannot cancel: already shipped"); }
}
class CancelledState {
constructor(order) { this.order = order; }
pay() { console.log("Cannot pay: order cancelled"); }
ship() { console.log("Cannot ship: order cancelled"); }
cancel() { console.log("Already cancelled"); }
}
const order = new Order();
order.pay();
order.ship();
order.cancel(); // Cannot cancel: already shipped
Java
interface OrderState {
void pay(Order order);
void ship(Order order);
void cancel(Order order);
}
class PendingState implements OrderState {
public void pay(Order order) {
System.out.println("Processing payment...");
order.transitionTo(new PaidState());
}
public void ship(Order order) {
System.out.println("Cannot ship: payment pending");
}
public void cancel(Order order) {
System.out.println("Order cancelled");
order.transitionTo(new CancelledState());
}
}
class PaidState implements OrderState {
public void pay(Order order) {
System.out.println("Already paid");
}
public void ship(Order order) {
System.out.println("Shipping order...");
order.transitionTo(new ShippedState());
}
public void cancel(Order order) {
System.out.println("Issuing refund...");
order.transitionTo(new CancelledState());
}
}
class ShippedState implements OrderState {
public void pay(Order order) {
System.out.println("Already paid");
}
public void ship(Order order) {
System.out.println("Already shipped");
}
public void cancel(Order order) {
System.out.println("Cannot cancel: already shipped");
}
}
class CancelledState implements OrderState {
public void pay(Order order) {
System.out.println("Cannot pay: order cancelled");
}
public void ship(Order order) {
System.out.println("Cannot ship: order cancelled");
}
public void cancel(Order order) {
System.out.println("Already cancelled");
}
}
class Order {
private OrderState state = new PendingState();
public void transitionTo(OrderState state) {
this.state = state;
}
public void pay() { state.pay(this); }
public void ship() { state.ship(this); }
public void cancel() { state.cancel(this); }
}
// Usage
Order order = new Order();
order.pay();
order.ship();
order.cancel(); // Cannot cancel: already shipped
Explanation
The pattern has three roles:
- Context (
Order): holds a reference to the current state and delegates every state-dependent call to it. It knows nothing about which transitions are legal. - State interface (
OrderState): the contract every state fulfills: one method per event the machine responds to. - Concrete states (
PendingState,PaidState,ShippedState,CancelledState): implement the behavior for their state and perform transitions by swapping the context’s state reference.
The GoF book calls this “Objects for States” and its canonical example is TCPConnection: a socket object whose open(), acknowledge(), and close() all behave differently depending on whether its state is Listen, Established, or Closed. Same idea as our Order, at protocol level.
Who owns the transitions?
The classic GoF version (shown above) lets states decide the next state. It’s honest about trade-offs:
- State-owned (this example): each state class knows its successors. Transitions live next to the behavior, but concrete states are coupled to each other;
PendingStatecan’t compile withoutPaidState. - Context-owned: the
Orderconsults a transition table and states never reference each other. States become reusable across machines, but the context regains conditional logic, which is what we were escaping. - Table-driven: transitions live in data (
transitions[state][event] = next), not classes. Best when you’ve got many states with sparse behavior (see Variants).
Entry and exit actions
When transitions need side effects (audit logs, metrics, releasing a seat), put them in enter()/exit() hooks on the state interface, and have transition_to call them:
def transition_to(self, state):
if self._state:
self._state.exit(self)
self._state = state
self._state.enter(self)
enter() runs after the swap, exit() before. This is where a pending order logs “left pending after 3 days” or a paid state triggers the “payment confirmed” email: effects that belong to the transition, not to whoever called pay().
Variants
| Variant | Description | Use Case |
|---|---|---|
| Simple State Machine | States handle transitions | Small number of states |
| Hierarchical State Machine | States can have substates | Complex nested behavior |
| State Table | Transitions stored in a lookup table | Many states with predictable transitions |
| Pushdown Automaton | Stack-based state history | Undo/reversible state transitions. See Memento |
For the table-driven variant, the whole machine collapses into a dict; worth it once you pass roughly six states and the per-state logic stays thin:
TRANSITIONS = {
("pending", "pay"): "paid",
("paid", "ship"): "shipped",
("pending", "cancel"): "cancelled",
("paid", "cancel"): "cancelled",
}
class InvalidTransition(Exception):
pass
class Order:
def __init__(self):
self.state = "pending"
def handle(self, event):
next_state = TRANSITIONS.get((self.state, event))
if next_state is None:
raise InvalidTransition(f"{event} not allowed in {self.state}")
self.state = next_state
This version keeps the legality matrix in one readable place (you can even render it in docs), but you lose the per-state hooks and have to attach side effects elsewhere. For genuinely large machines, libraries like transitions (Python), xstate (TypeScript), or Spring StateMachine do this for you and add guards, hierarchies, and visualization.
Testing State Machines
The pattern pays for itself at test time: each state is a unit, and transitions are assertions on the context:
def test_pending_order_becomes_paid():
order = Order()
order.pay()
assert isinstance(order._state, PaidState)
def test_cannot_ship_before_payment():
order = Order()
order.ship()
assert isinstance(order._state, PendingState)
def test_cancelled_order_rejects_everything():
order = Order()
order.cancel()
order.pay()
assert isinstance(order._state, CancelledState)
Test the matrix, not the wiring: one test per (state, event) pair that matters, plus the terminal states. The InvalidTransition path in the table variant is a good property-based test candidate: any pair outside TRANSITIONS must raise.
What Works
- Delegate all state-dependent behavior to state objects, keeping the context thin
- Make state objects immutable or recreate them on transition to avoid shared state bugs
- Use enums for well-defined states in languages that support them (Java, TypeScript)
- Add entry/exit hooks when transitions need side effects (logging, analytics)
- Consider a transition table once you’ve got many states and transitions become hard to manage
Common Mistakes
- Silent invalid transitions. Returning
nullor logging quietly on an illegal event hides bugs; raiseInvalidTransitionor at least make the no-op loud. - Sharing a single state instance across contexts. If
PaidStateholds mutable fields, two orders share bugs. Instantiate per order or keep states stateless. - Business logic inside the context.
Ordergrowingif state == "paid": apply_discount()is the original problem creeping back: the discount belongs toPaidStateor a collaborator. - States calling back into unrelated context methods. If a state starts needing
order.user.emailandorder.inventory.reserve(), that’s a sign you need the Mediator or a proper domain service, not deeper coupling. - A context that exposes
transition_topublicly. Callers doingorder.transition_to(ShippedState())bypass every rule; keep transitions triggered only through events. (In the examples above,transition_toexists for the states’ internal use; think of it as plumbing, not public API._transition_toworks too.)
See Also
- Command Pattern — encapsulates requests as objects; pairs naturally with State when transitions trigger commands
- Observer Pattern — for notifying listeners when a transition happens
- Memento Pattern — snapshots state for undo; complements the pushdown-automaton variant
- State Machine UI Pattern — the frontend sibling: finite state machines and statecharts for UI components
- Companion repository — the order state machine as runnable Python, JavaScript, and Java files, including the table-driven variant
Further Reading
- Refactoring.Guru — State — UML diagrams and a document-editing walkthrough
- Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — the original definition (Gamma, Helm, Johnson, Vlissides, 1994)
- Python transitions library — table-driven state machines with callbacks, when classes get heavy
- XState documentation — statecharts for TypeScript and UI applications
Frequently Asked Questions
What is the difference between State and Strategy?
Both encapsulate behavior in interchangeable objects, but the mechanics differ. State changes what the object does as its internal state moves through a lifecycle. Nobody picks anything. Strategy works the other way around: the caller picks the algorithm from outside, and the object doesn't care which one it got.
Should states know about each other?
In the classic implementation, states trigger transitions to other states. An alternative is to let the context or a transition table manage transitions, keeping states decoupled.
Can I use enums instead of classes for simple state machines?
Yes. For the simple case, an enum plus a switch does the job. Reach for the full pattern when states carry real behavior or you expect to keep adding them; an enum can't grow an enter() hook.
How does this relate to finite state machines and XState?
The GoF pattern is one way to implement a finite state machine with classes: states as objects, events as method calls, transitions as object swaps. Front-end tools like XState take the same concept further with statecharts (hierarchies, parallel states, visual editing), and the State Machine UI Pattern covers that side. If your order has nested states like shipped.in_transit and shipped.delivered, a statechart library is usually the better fit than hand-rolled classes.
How do I add a new state without breaking existing ones?
Create the class, implement the three events, and wire the inbound transition from the state that produces it (e.g., a RefundedState reachable from PaidState.cancel()). Nothing else needs recompiling, which is exactly the point: the switch version would have forced you through every method.
Is a state ever allowed to call back into the context?
Yes — the context reference is the only channel a state has for triggering a transition. The smell is when it calls beyond transition_to and the state-specific helpers: reaching for order.payment_gateway from inside PaidState means the state knows too much about the context's internals. Pass the context but treat it as a handle for transitions, not as a grab-bag of services.
Related Resources
Strategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
PatternCommand Pattern
Encapsulate a request as an object, letting you parameterize clients with queues, logs, and undoable operations. A behavioral design pattern.
PatternObserver Pattern
Define a subscription mechanism to notify multiple objects about events. A behavioral design pattern for event-driven communication.
PatternMediator Pattern
Define an object that encapsulates how a set of objects interact. A behavioral design pattern for reducing chaotic dependencies.
PatternMemento Pattern
Capture and restore an object's internal state without violating encapsulation. A behavioral design pattern for undo/redo.