Intent

Allow an object to alter its behavior when its internal state changes. The object should appear, to the outside world, as if it changed its class.

The Problem

You ever inherit an Order model where every interesting method starts with the same six lines?

PHP
public function ship(): void
{
    if ($this->status === 'pending')   throw new CannotShipUnpaidOrder();
    if ($this->status === 'cancelled') throw new CannotShipCancelledOrder();
    if ($this->status === 'shipped')   throw new AlreadyShipped();
    if ($this->status === 'delivered') throw new AlreadyShipped();
    // ...actually ship
    $this->status = 'shipped';
}

pay() does the same dance. So does cancel(). So does refund(). The status field has become a load-bearing string, and every new method has to re-derive which transitions are legal. Add a sixth status next sprint and you're hunting switch blocks across forty files.

The Solution

State says: pull each status into its own class with the methods that are valid in that state. The Context (the Order) holds a reference to one state object and delegates to it. Transitions become part of the state's response — the PendingState.pay() method returns a new PaidState, and the Context swaps its reference.

PHP
interface OrderState
{
    public function pay(Order $order): OrderState;
    public function ship(Order $order): OrderState;
    public function cancel(Order $order): OrderState;
}

The Order becomes a thin shell that says "ask my state." The if ladder evaporates because each state knows what it accepts and what it refuses. Adding a new state is a new class plus an entry in two existing transitions — never a sweep across the whole codebase.

Real-World Analogy

A traffic light. The metal box is the same at every intersection — but its behavior is completely different depending on which colour is currently lit. Red refuses cars and waves through pedestrians. Green does the opposite. Yellow is its own short, loud, ambiguous thing. The transitions are baked into each colour: red knows it becomes green next, green knows it becomes yellow.

You don't get one giant switch (currentColour) running every second. You get three small light-coloured rules and a clock.

Structure

State pattern: an Order context delegates to an OrderState interface that PendingState, PaidState, and ShippedState implement.
State: the Context delegates to a State; each Concrete State knows its valid transitions.

Three roles you'll see in every State implementation:

  • Context — the class clients call. Holds a reference to the current state. In our example: Order.
  • State — the interface every state agrees on. Here: OrderState. It declares one method per event the Context can receive.
  • Concrete State — one class per state: PendingState, PaidState, ShippedState, CancelledState. Each one decides what to do (or refuse) for every event, and which state to transition to.

The diagram looks identical to Strategy. The semantic difference: the state object decides when to swap itself out, where in Strategy the client always picks.

Code Examples

Here's the same Order lifecycle in five languages. Notice that every state exposes the same methods — but each one accepts only the transitions that make sense from where it is.

interface OrderState {
  pay(order: Order): OrderState;
  ship(order: Order): OrderState;
  cancel(order: Order): OrderState;
}

class PendingState implements OrderState {
  pay(order: Order): OrderState    { /* charge card */ return new PaidState(); }
  ship(order: Order): OrderState   { throw new Error("Pay first."); }
  cancel(order: Order): OrderState { return new CancelledState(); }
}

class PaidState implements OrderState {
  pay(order: Order): OrderState    { throw new Error("Already paid."); }
  ship(order: Order): OrderState   { /* dispatch */ return new ShippedState(); }
  cancel(order: Order): OrderState { /* refund */ return new CancelledState(); }
}

export class Order {
  constructor(private state: OrderState = new PendingState()) {}
  pay()    { this.state = this.state.pay(this); }
  ship()   { this.state = this.state.ship(this); }
  cancel() { this.state = this.state.cancel(this); }
}
from abc import ABC, abstractmethod

class OrderState(ABC):
    @abstractmethod
    def pay(self, order):    ...
    @abstractmethod
    def ship(self, order):   ...
    @abstractmethod
    def cancel(self, order): ...

class PendingState(OrderState):
    def pay(self, order):    return PaidState()
    def ship(self, order):   raise RuntimeError("Pay first.")
    def cancel(self, order): return CancelledState()

class PaidState(OrderState):
    def pay(self, order):    raise RuntimeError("Already paid.")
    def ship(self, order):   return ShippedState()
    def cancel(self, order): return CancelledState()

class Order:
    def __init__(self, state=None):
        self._state = state or PendingState()
    def pay(self):    self._state = self._state.pay(self)
    def ship(self):   self._state = self._state.ship(self)
    def cancel(self): self._state = self._state.cancel(self)
public interface OrderState {
    OrderState pay(Order order);
    OrderState ship(Order order);
    OrderState cancel(Order order);
}

public final class PendingState implements OrderState {
    @Override public OrderState pay(Order order)    { return new PaidState(); }
    @Override public OrderState ship(Order order)   { throw new IllegalStateException("Pay first."); }
    @Override public OrderState cancel(Order order) { return new CancelledState(); }
}

public final class PaidState implements OrderState {
    @Override public OrderState pay(Order order)    { throw new IllegalStateException("Already paid."); }
    @Override public OrderState ship(Order order)   { return new ShippedState(); }
    @Override public OrderState cancel(Order order) { return new CancelledState(); }
}

public final class Order {
    private OrderState state = new PendingState();

    public void pay()    { state = state.pay(this); }
    public void ship()   { state = state.ship(this); }
    public void cancel() { state = state.cancel(this); }
}
<?php

namespace App\Domain;

interface OrderState
{
    public function pay(Order $order):    OrderState;
    public function ship(Order $order):   OrderState;
    public function cancel(Order $order): OrderState;
}

final class PendingState implements OrderState
{
    public function pay(Order $order):    OrderState { return new PaidState(); }
    public function ship(Order $order):   OrderState { throw new \DomainException("Pay first."); }
    public function cancel(Order $order): OrderState { return new CancelledState(); }
}

final class PaidState implements OrderState
{
    public function pay(Order $order):    OrderState { throw new \DomainException("Already paid."); }
    public function ship(Order $order):   OrderState { return new ShippedState(); }
    public function cancel(Order $order): OrderState { return new CancelledState(); }
}

final class Order
{
    public function __construct(private OrderState $state = new PendingState()) {}

    public function pay():    void { $this->state = $this->state->pay($this); }
    public function ship():   void { $this->state = $this->state->ship($this); }
    public function cancel(): void { $this->state = $this->state->cancel($this); }
}
package order

type OrderState interface {
    Pay(o *Order)    OrderState
    Ship(o *Order)   OrderState
    Cancel(o *Order) OrderState
}

type PendingState struct{}

func (PendingState) Pay(o *Order) OrderState    { return PaidState{} }
func (PendingState) Ship(o *Order) OrderState   { panic("pay first") }
func (PendingState) Cancel(o *Order) OrderState { return CancelledState{} }

type PaidState struct{}

func (PaidState) Pay(o *Order) OrderState    { panic("already paid") }
func (PaidState) Ship(o *Order) OrderState   { return ShippedState{} }
func (PaidState) Cancel(o *Order) OrderState { return CancelledState{} }

type Order struct {
    state OrderState
}

func NewOrder() *Order { return &Order{state: PendingState{}} }

func (o *Order) Pay()    { o.state = o.state.Pay(o) }
func (o *Order) Ship()   { o.state = o.state.Ship(o) }
func (o *Order) Cancel() { o.state = o.state.Cancel(o) }

The Order class never asks "what state am I in?" again. The if ladder is gone, and the rules of the lifecycle are now spread across small, focused, individually testable classes.

When to Use It

Reach for State when you can answer "yes" to any of these:

  • Your object's behavior depends on its status and that status keeps growing. Subscriptions, orders, documents, conversations, deployments — anywhere a lifecycle has more than three meaningful phases.
  • The same status check repeats in every method. When if (status === ...) shows up at the top of pay(), ship(), refund(), and archive(), the status itself wants to be an object.
  • Transitions have business logic, not just rules. Sending a confirmation email when paid, locking inventory when shipped, refunding when cancelled — that logic lives naturally inside the state that causes the transition.
  • Different states have different invariants. A paid order has a paid_at; a shipped order has a tracking_number. State objects can carry the data their phase actually needs.

If your status only ever has two values and the only difference is a boolean check, a flag is fine. State earns its keep when transitions get rich.

Pros and Cons

Pros

  • The growing switch disappears. Each state's behavior lives in one focused class.
  • New states drop in without touching existing ones.
  • Each state is small, isolated, and easy to test on its own.
  • Transitions are visible in the code that owns them, not buried inside a coordinator.

Cons

  • More files. For a status with two values, the abstraction is overkill.
  • The Context still has to know about the state interface — and clients have to know which event method to call.
  • Sharing state between, well, States can get awkward if they need access to the Context's data. Plan the constructor early.

Pro Tips

  • Make the State methods return the next state. Pure transitions are easier to read and test than mutating the Context inside the state.
  • Keep the Context dumb. It should only know the current state and how to delegate. Any business logic in the Context is a bug in disguise.
  • One method per event, not per state. The interface lists the events the Context can receive — pay, ship, cancel. Each state then decides what to do for every event.
  • Persist the state name, rehydrate the object. When you load an Order from the database, look up the right OrderState from a map — don't try to serialize the object itself.

Relations with Other Patterns

  • Strategy has the same UML diagram. The intent is the difference: Strategy lets the client swap algorithms; State lets the Context swap its own behavior as it transitions.
  • Command pairs well with State for representing the event that triggered a transition — useful when you want an audit log of "who did what to push this order from Pending to Paid."
  • State machines as a formal model are a generalisation of this pattern; if your transitions get gnarly enough to need a diagram, draw one — the State pattern is the implementation, but the diagram is the design tool.

Final Tips

The first time State clicked for me was after spending two days hunting a bug where a cancelled order somehow got shipped. The cancel() method had set status = 'cancelled', but a queued ship() job that had been enqueued before the cancellation ran ten minutes later — and the ship() method only checked if (status === 'pending'), not !== 'cancelled'. After we refactored to State, the bug couldn't recur: a CancelledState simply doesn't expose ship() as a valid action.

That's the deep promise of this pattern. Not just cleaner code — unrepresentable invalid states. Use it when the lifecycle starts costing you sleep.