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
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]
abstract class State { // default: every action is illegal unless a state allows it
abstract readonly name: string;
pay(_order: Order): string { return `cannot pay a ${this.name} order`; }
ship(_order: Order): string { return `cannot ship a ${this.name} order`; }
cancel(_order: Order): string { return `cannot cancel a ${this.name} order`; }
}
class Pending extends State {
readonly name = "pending";
pay(order: Order) { order.state = new Paid(); return "paid"; }
cancel(order: Order) { order.state = new Cancelled(); return "cancelled, nothing to refund"; }
}
class Paid extends State {
readonly name = "paid";
ship(order: Order) { order.state = new Shipped(); return "shipped"; }
cancel(order: Order) { order.state = new Cancelled(); return "cancelled, refund issued"; }
}
class Shipped extends State { readonly name = "shipped"; }
class Cancelled extends State { readonly name = "cancelled"; }
class Order { // context: delegates to its current state
state: State = new Pending();
act(action: "pay" | "ship" | "cancel"): string {
const result = this.state[action](this);
return `${action.padEnd(6)} -> ${result.padEnd(30)} [now ${this.state.name}]`;
}
}
const order = new Order();
for (const action of ["ship", "pay", "pay", "ship", "cancel"] as const) console.log(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]
public class Main {
public static void main(String[] args) {
Order order = new Order();
for (String action : new String[]{"ship", "pay", "pay", "ship", "cancel"}) System.out.println(order.act(action));
}
}
abstract class State { // default: every action is illegal unless a state allows it
abstract String name();
String pay(Order order) { return "cannot pay a " + name() + " order"; }
String ship(Order order) { return "cannot ship a " + name() + " order"; }
String cancel(Order order) { return "cannot cancel a " + name() + " order"; }
}
class Pending extends State {
String name() { return "pending"; }
String pay(Order order) { order.state = new Paid(); return "paid"; }
String cancel(Order order) { order.state = new Cancelled(); return "cancelled, nothing to refund"; }
}
class Paid extends State {
String name() { return "paid"; }
String ship(Order order) { order.state = new Shipped(); return "shipped"; }
String cancel(Order order) { order.state = new Cancelled(); return "cancelled, refund issued"; }
}
class Shipped extends State { String name() { return "shipped"; } }
class Cancelled extends State { String name() { return "cancelled"; } }
class Order { // context: delegates to its current state
State state = new Pending();
String act(String action) {
String result = switch (action) {
case "pay" -> state.pay(this);
case "ship" -> state.ship(this);
default -> state.cancel(this);
};
return String.format("%-6s -> %-30s [now %s]", action, result, state.name());
}
}
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]
Run this on your computer — see “Get Started” for setup.
<?php
abstract class State { // default: every action is illegal unless a state allows it
abstract public function name(): string;
public function pay(Order $order): string { return "cannot pay a {$this->name()} order"; }
public function ship(Order $order): string { return "cannot ship a {$this->name()} order"; }
public function cancel(Order $order): string { return "cannot cancel a {$this->name()} order"; }
}
class Pending extends State {
public function name(): string { return "pending"; }
public function pay(Order $order): string { $order->state = new Paid(); return "paid"; }
public function cancel(Order $order): string { $order->state = new Cancelled(); return "cancelled, nothing to refund"; }
}
class Paid extends State {
public function name(): string { return "paid"; }
public function ship(Order $order): string { $order->state = new Shipped(); return "shipped"; }
public function cancel(Order $order): string { $order->state = new Cancelled(); return "cancelled, refund issued"; }
}
class Shipped extends State { public function name(): string { return "shipped"; } }
class Cancelled extends State { public function name(): string { return "cancelled"; } }
class Order { // context: delegates to its current state
public State $state;
public function __construct() { $this->state = new Pending(); }
public function act(string $action): string {
$result = $this->state->$action($this);
return sprintf("%-6s -> %-30s [now %s]", $action, $result, $this->state->name());
}
}
$order = new Order();
foreach (["ship", "pay", "pay", "ship", "cancel"] as $action) echo $order->act($action), "\n";
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/ifon 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
switchstatements 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.