Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work

State

Let an object change its behaviour when its internal state changes by moving each state's rules into its own class, instead of a switch statement in every method.

The problem

An order can be pending, paid, shipped or cancelled. Every method starts with a switch on the status: pay() is allowed only when pending, ship() only when paid, cancel() when pending or paid but not shipped. There are four methods with four branches each, and a new status like refunded means editing all of them.

A bug slips in: one branch forgets a check, and shipped orders can be cancelled.

Real-world analogy

A vending machine. Pressing "dispense" does nothing when no coin is inserted, gives a snack when paid, and shows "sold out" when empty. Same button, different behaviour depending on the machine's state, and inserting a coin moves it to a new state.

Without the pattern

def cancel(self):
    if self.status == "pending" or self.status == "paid":
        self.status = "cancelled"
    elif self.status == "shipped":
        raise ValueError("too late")
    elif self.status == "cancelled":
        raise ValueError("already cancelled")
    # ...and the same kind of switch in pay(), ship(), refund()

The pattern

  • Context (Order): holds a reference to its current state object and delegates actions to it.
  • State interface: one method per action (pay, ship, cancel).
  • Concrete states (Pending, Paid, Shipped, Cancelled): each implements the rules for its state and decides the transition by telling the context which state comes next.

All the rules for "paid" are in one class. A new state is a new class, and illegal actions fail by default.

The code

An order whose behaviour depends on its stateTry it yourself
class State:  # default: every action is illegal unless a state allows it
    name = "?"

    def pay(self, order):
        return f"cannot pay a {self.name} order"

    def ship(self, order):
        return f"cannot ship a {self.name} order"

    def cancel(self, order):
        return f"cannot cancel a {self.name} order"


class Pending(State):
    name = "pending"

    def pay(self, order):
        order.state = Paid()
        return "paid"

    def cancel(self, order):
        order.state = Cancelled()
        return "cancelled, nothing to refund"


class Paid(State):
    name = "paid"

    def ship(self, order):
        order.state = Shipped()
        return "shipped"

    def cancel(self, order):
        order.state = Cancelled()
        return "cancelled, refund issued"


class Shipped(State):
    name = "shipped"


class Cancelled(State):
    name = "cancelled"


class Order:  # context: delegates to its current state
    def __init__(self):
        self.state = Pending()

    def act(self, action):
        result = getattr(self.state, action)(self)
        return f"{action:<6} -> {result:<30} [now {self.state.name}]"


order = Order()
for action in ["ship", "pay", "pay", "ship", "cancel"]:
    print(order.act(action))

Output

ship   -> cannot ship a pending order    [now pending]
pay    -> paid                           [now paid]
pay    -> cannot pay a paid order        [now paid]
ship   -> shipped                        [now shipped]
cancel -> cannot cancel a shipped order  [now shipped]

Shipping before payment, paying twice and cancelling after shipping are all rejected, without a single if on the status. The rules for each state sit in one place.

When to use it

  • An object's behaviour depends on its state and the same switch/if on a status field appears in many methods.
  • There are clear states and transitions: orders, documents (draft, review, published), connections, game characters, approval workflows.
  • You want illegal transitions to be impossible by construction.

When not to use it

  • There are two or three states with trivial rules. A status enum and a couple of ifs are clearer.
  • The workflow is mostly data (which transitions are allowed), not behaviour. A transition table or a state-machine library (XState, Spring Statemachine, a Laravel state package) may suit better.
  • Persisting state: store the state name in the database and recreate the object when loading; don't serialise state objects.

You've already used it

  • TCP connections (LISTEN, ESTABLISHED, CLOSED) are the textbook example.
  • UI state machines: XState, and component states like idle, loading, success and error.
  • Workflow libraries for orders, payments and publishing workflows in Laravel, Django and Spring.

Key points

  • State moves each state's behaviour into its own class; the context delegates to the current state.
  • States decide transitions, so illegal actions fail by default.
  • It removes repeated switch statements on a status field.
  • Strategy looks similar, but strategies are chosen by the client and don't know about each other; states replace themselves.
  • Further reading: Refactoring Guru: State.

Practice questions

What problem does the State pattern solve?

Answer

Methods full of switch/if checks on a status field. Each state becomes a class implementing the actions allowed in that state and deciding the next state, so the context simply delegates to its current state object.

What is the difference between State and Strategy?

Answer

Both delegate to an interchangeable object. In Strategy, the client picks an algorithm and strategies are independent of each other. In State, the object's own state objects trigger transitions to other states, so behaviour changes as the object's lifecycle progresses.

How would you persist an object that uses the State pattern?

Answer

Store the state's name (for example status = 'paid') in the database and map it back to the state object when loading. State objects themselves shouldn't be serialised.

Open in the app to mark lessons complete, take notes and track your progress.