StackPractices
intermediate By Mathias Paulenko

State Pattern

Allow an object to alter its behavior when its internal state changes. A behavioral design pattern for finite state machines.

Topics: design

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

VariantDescriptionUse Case
Simple State MachineStates handle transitionsSmall number of states
Hierarchical State MachineStates can have substatesComplex nested behavior
State TableTransitions stored in a lookup tableMany states with predictable transitions
Pushdown AutomatonStack-based state historyUndo/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.