State Pattern
Allow an object to alter its behavior when its internal state changes. A behavioral design pattern for finite state machines.
Overview
The State Pattern is a behavioral design pattern that lets an object alter its behavior when its internal state changes. Instead of large switch statements, each state is encapsulated in its own class with state-specific behavior.
When to Use
Use the State Pattern when:
- An object’s behavior depends on its state and must change at runtime
- You have large conditional statements that switch behavior based on state
- States have complex transitions with entry/exit actions
- You want to add new states without modifying existing state classes
Solution
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 State Pattern involves two roles:
- Context (
Order): Maintains a reference to the current state and delegates behavior to it - State Interface (
OrderState): Defines the contract for state-specific behavior - Concrete States (
PendingState,PaidState, etc.): Implement behavior specific to each state and handle transitions
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 |
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 when you have many states and transitions become hard to manage
Common Mistakes
- Duplicating transition logic across multiple state classes instead of centralizing it
- Allowing the context to modify state directly instead of delegating to the current state
- Forgetting to handle invalid transitions gracefully, leading to runtime errors
- Creating circular state transitions that can cause infinite loops
- Mixing state logic with business logic, making the pattern hard to test
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Key Takeaways
- Apply state pattern when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Common Production Pitfalls
- Applying the pattern where no abstraction is needed, adding accidental complexity.
- Letting the pattern leak into unrelated modules and blur ownership boundaries.
- Over-engineering the first implementation instead of starting simple and measuring pain.
- Skipping contract tests, so refactors silently break consumers.
- Ignoring failure modes that the pattern does not cover.
- Using the pattern as a default instead of choosing the right tool for the current scale.
- Forgetting to document when to stop using the pattern and what replaces it.
- Missing observability around the pattern’s performance and error propagation.
Frequently Asked Questions
What is the difference between State and Strategy?
Both encapsulate behavior in interchangeable objects. State is about changing behavior based on internal state transitions. Strategy is about selecting an algorithm from the outside. The intent differs: State manages lifecycle, Strategy provides options.
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 simple cases, an enum with a switch statement is sufficient. Use the full State Pattern when states have complex behavior or you expect frequent additions.
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.