State
The State pattern gives each state of an object its own class, so the rules for "what can happen now" live in one place per state and impossible combinations can't exist. This page turns a Pedal bike's lifecycle into four state classes, and lets you drive a bike through them.
1. The pain
A Pedal bike can be docked, in use, reported faulty, or in the workshop. What it may do depends on which: you can unlock a docked bike but not a faulty one; you can return a bike in use but not one in repair. The first version tracked this with three booleans, in_use, faulty and in_repair, and every method checked them.
Five methods each repeated the same checks in slightly different orders. One of them, give_back(), forgot in_repair, so a bike on the workshop bench was marked docked and rented out. And nothing stopped faulty and in_use both being true, a state no one had designed for.
2. The idea in one picture
State gives each state its own class, and lets the object (the context) delegate every event to its current state. Docked.unlock() returns InUse; Faulty.unlock() refuses with a reason. The bike holds exactly one state at a time, so impossible combinations can't exist, and each state's rules are in one place.
State and strategy look the same in code: an object delegating to a swappable part. The difference is who swaps it. A strategy is chosen from outside; a state chooses its successor as events arrive.
3. The roles
| Role | Its job | In Pedal |
|---|---|---|
| Context | Holds the current state and passes each event to it. | Bike with handle(event) |
| State | The interface: one method per event. | BikeState: unlock(), give_back(), report_fault()… |
| Concrete state | The rules for one state, including which state comes next. | Docked, InUse, Faulty, InRepair |
4. Before and after
The bike's rules both ways (Python sketch; the full program is in section 5).
Without the pattern
def give_back(self):
if self.faulty:
return "refused"
if not self.in_use:
return "refused"
# forgot: if self.in_repair
self.in_use = False
def unlock(self):
if self.in_use or self.faulty \
or self.in_repair:
return "refused"
self.in_use = TrueWith it
class InUse(BikeState):
def give_back(self):
return Docked()
def report_fault(self):
return Faulty()
class InRepair(BikeState):
def finish_repair(self):
return Docked()
# give_back() not defined here:
# the base class refuses it| The next change | Without the pattern | With it |
|---|---|---|
| Add a "reserved" state | A fourth boolean, checked in every method | A Reserved class, and Docked gains reserve() |
| Allow returning a faulty bike | Find every check of faulty | Add give_back() to Faulty |
| Explain the rules to a new engineer | Read five methods | Read one class per state, or draw it |
With one class per state the transitions are the code, so they can be read, drawn and tested one state at a time.
5. The code
Four state classes each define the events they accept and return the next state; the base class refuses everything else with a reason. The bike just delegates.
from __future__ import annotations
# ---- One class per state. Each answers every event: move to the next state, or refuse. ----
class BikeState:
name = ""
def unlock(self) -> BikeState | str: return f"refused ({self.why_not()})"
def give_back(self) -> BikeState | str: return f"refused ({self.why_not()})"
def report_fault(self) -> BikeState | str: return f"refused ({self.why_not()})"
def start_repair(self) -> BikeState | str: return f"refused ({self.why_not()})"
def finish_repair(self) -> BikeState | str: return f"refused ({self.why_not()})"
def why_not(self) -> str: return f"bike is {self.name}"
class Docked(BikeState):
name = "docked"
def unlock(self) -> BikeState | str: return InUse()
def report_fault(self) -> BikeState | str: return Faulty()
class InUse(BikeState):
name = "in use"
def give_back(self) -> BikeState | str: return Docked()
def report_fault(self) -> BikeState | str: return Faulty()
def why_not(self) -> str: return "already in use"
class Faulty(BikeState):
name = "faulty"
def start_repair(self) -> BikeState | str: return InRepair()
def why_not(self) -> str: return "reported faulty"
class InRepair(BikeState):
name = "in repair"
def finish_repair(self) -> BikeState | str: return Docked()
# ---- The context: delegates each event to its current state and moves on when told to. ----
class Bike:
def __init__(self, bike_id: str) -> None:
self.bike_id = bike_id
self.state: BikeState = Docked()
def handle(self, event: str) -> str:
before = self.state.name
result = getattr(self.state, event)()
if isinstance(result, BikeState):
self.state = result
return f"{self.bike_id} {before}: {event} -> {result.name}"
return f"{self.bike_id} {before}: {event} -> {result}"
if __name__ == "__main__":
bike = Bike("B-17")
for event in ["unlock", "unlock", "give_back", "report_fault", "unlock", "start_repair", "give_back", "finish_repair", "unlock"]:
print(bike.handle(event))
type Event = "unlock" | "giveBack" | "reportFault" | "startRepair" | "finishRepair";
// ---- One class per state. Each answers every event: move to the next state, or refuse. ----
abstract class BikeState {
abstract readonly name: string;
unlock(): BikeState | string { return `refused (${this.whyNot()})`; }
giveBack(): BikeState | string { return `refused (${this.whyNot()})`; }
reportFault(): BikeState | string { return `refused (${this.whyNot()})`; }
startRepair(): BikeState | string { return `refused (${this.whyNot()})`; }
finishRepair(): BikeState | string { return `refused (${this.whyNot()})`; }
whyNot(): string { return `bike is ${this.name}`; }
}
class Docked extends BikeState {
readonly name = "docked";
unlock(): BikeState | string { return new InUse(); }
reportFault(): BikeState | string { return new Faulty(); }
}
class InUse extends BikeState {
readonly name = "in use";
giveBack(): BikeState | string { return new Docked(); }
reportFault(): BikeState | string { return new Faulty(); }
whyNot(): string { return "already in use"; }
}
class Faulty extends BikeState {
readonly name = "faulty";
startRepair(): BikeState | string { return new InRepair(); }
whyNot(): string { return "reported faulty"; }
}
class InRepair extends BikeState {
readonly name = "in repair";
finishRepair(): BikeState | string { return new Docked(); }
}
// ---- The context: delegates each event to its current state and moves on when told to. ----
class Bike {
private state: BikeState = new Docked();
constructor(private readonly bikeId: string) {}
handle(event: Event): string {
const before = this.state.name, label = event.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
const result = this.state[event]();
if (result instanceof BikeState) {
this.state = result;
return `${this.bikeId} ${before}: ${label} -> ${result.name}`;
}
return `${this.bikeId} ${before}: ${label} -> ${result}`;
}
}
const bike = new Bike("B-17");
const events: Event[] = ["unlock", "unlock", "giveBack", "reportFault", "unlock", "startRepair", "giveBack", "finishRepair", "unlock"];
for (const e of events) console.log(bike.handle(e));import java.util.List;
public class Main {
// ---- One class per state. Each answers every event: move to the next state, or refuse. ----
abstract static class BikeState {
abstract String name();
Object unlock() { return "refused (" + whyNot() + ")"; }
Object giveBack() { return "refused (" + whyNot() + ")"; }
Object reportFault() { return "refused (" + whyNot() + ")"; }
Object startRepair() { return "refused (" + whyNot() + ")"; }
Object finishRepair() { return "refused (" + whyNot() + ")"; }
String whyNot() { return "bike is " + name(); }
}
static class Docked extends BikeState {
String name() { return "docked"; }
Object unlock() { return new InUse(); }
Object reportFault() { return new Faulty(); }
}
static class InUse extends BikeState {
String name() { return "in use"; }
Object giveBack() { return new Docked(); }
Object reportFault() { return new Faulty(); }
String whyNot() { return "already in use"; }
}
static class Faulty extends BikeState {
String name() { return "faulty"; }
Object startRepair() { return new InRepair(); }
String whyNot() { return "reported faulty"; }
}
static class InRepair extends BikeState {
String name() { return "in repair"; }
Object finishRepair() { return new Docked(); }
}
// ---- The context: delegates each event to its current state and moves on when told to. ----
static class Bike {
private final String bikeId;
private BikeState state = new Docked();
Bike(String bikeId) { this.bikeId = bikeId; }
String handle(String event) {
String before = state.name();
Object result = switch (event) {
case "unlock" -> state.unlock();
case "give_back" -> state.giveBack();
case "report_fault" -> state.reportFault();
case "start_repair" -> state.startRepair();
case "finish_repair" -> state.finishRepair();
default -> throw new IllegalArgumentException(event);
};
if (result instanceof BikeState next) {
state = next;
return bikeId + " " + before + ": " + event + " -> " + next.name();
}
return bikeId + " " + before + ": " + event + " -> " + result;
}
}
public static void main(String[] args) {
Bike bike = new Bike("B-17");
for (String e : List.of("unlock", "unlock", "give_back", "report_fault", "unlock", "start_repair", "give_back", "finish_repair", "unlock"))
System.out.println(bike.handle(e));
}
}Output (the same in all three languages)
B-17 docked: unlock -> in use B-17 in use: unlock -> refused (already in use) B-17 in use: give_back -> docked B-17 docked: report_fault -> faulty B-17 faulty: unlock -> refused (reported faulty) B-17 faulty: start_repair -> in repair B-17 in repair: give_back -> refused (bike is in repair) B-17 in repair: finish_repair -> docked B-17 docked: unlock -> in use
6. Try it
Drive a bike through its states
Send events to bike B-17. The current state decides what each one does.
The highlighted row is the current state's class. A dash means that class doesn't define the event, so it refuses.
7. When not to use it
What you get
- Impossible combinations disappear: one state at a time.
- Rules in one place per state: easy to read and test.
- Transitions are explicit: each state names its successors.
- New states are additive: mostly one new class.
What you pay
- More classes: one per state.
- Transitions are spread out: to see the whole machine, you read every class or draw it.
- States may need the context: passing it in couples them a little.
- Overkill for two states: a boolean is fine for on/off.
| If… | Use instead |
|---|---|
| The behaviour is chosen from outside, not by events | 13 Strategy |
| There are two states and simple rules | A boolean or an enum with a match |
| The machine is large, data-driven or edited by non-programmers | A transition table (state × event → next state) |
| States must survive restarts | Store the state name; rebuild the object on load |
8. How it goes wrong
InUse reaches into Bike to flip flags, the flags are back. Return the next state instead.if state is Docked and event == … in Bike puts the switch statement back.9. Remember it and say it
How this hook works: what “The light knows what's next” means
What the slogan means, phrase by phrase
- “The light”
- The context:
Bike. - “knows”
- Each state class holds its own rules.
- “what's next”
- It returns the following state.
Why this picture fits: Nobody models a traffic light with booleans for red, amber and green; each colour simply knows what follows.
The story → the concept
| The traffic light | The code |
|---|---|
| Red, amber, green | Docked, InUse, Faulty, InRepair |
| "Change" button | An event such as unlock |
| Red refuses to go straight to amber | Faulty refuses unlock |
| Adding flashing amber | Adding Reserved |
Watch it on an example
- B-17 is docked:
unlockmoves it to in use. - A fault is reported: it becomes faulty.
- Someone tries to unlock it:
Faultyrefuses, "reported faulty". No flags were checked.
The trap it saves you from
Booleans that can contradict each other, checked differently in every method.
It still works when…
Yes: order lifecycles, TCP connections, document approval flows and game characters are all state machines.
In an interview or code review
- Draw it first: four states and five events on the whiteboard, then say each state becomes a class.
- Say what it removes: booleans that can contradict each other, and checks repeated in every method.
- Contrast with strategy: the state picks its successor; a strategy is chosen from outside.
- Mention the alternative: for big or data-driven machines, a transition table; for persistence, store the state name.
10. Check yourself
What does the context do with an event?
A faulty bike gets unlock. Who refuses?
What bug class does State remove?
State vs strategy: who changes the delegate?
Adding a Reserved state touches…
11. Go deeper
| Resource | Why |
|---|---|
| 13 Strategy | Same structure; chosen from outside. |
| Low-level design track | Vending machines, elevators and parking lots: state machines in interview problems. |
Python enum | Naming states when a full pattern is too much. |
| Wikipedia: State pattern | Structure and relation to finite-state machines. |