Skip to content
SP StackPractices
intermediate

State Pattern

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

Topics: design

State Pattern

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

Best Practices

  • 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

Frequently Asked Questions

Q: What is the difference between State and Strategy? A: 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.

Q: Should states know about each other? A: 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.

Q: Can I use enums instead of classes for simple state machines? A: 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.