State machines and the State pattern
Behaviour that depends on a mode, without a wall of conditionals.
01The intuition
The problem it solves
A vending machine program has a few modes: waiting for a coin, holding money, dispensing, out of order. Every button press means something different in each mode. The obvious code keeps a mode string and checks it inside every method, so each method is a wall of conditions and each new mode means editing all of them. A bug hides easily: press "refund" while dispensing and the machine both drops the snack and returns the coins.
class Machine:
def __init__(self) -> None:
self.mode = "idle"
self.balance = 0
def press(self, button: str) -> None:
if self.mode == "idle":
if button == "coin": self.balance += 10; self.mode = "paid"
elif self.mode == "paid":
if button == "coin": self.balance += 10
elif button == "select": self.mode = "dispensing"; self.drop()
elif button == "refund": self.give_back(); self.mode = "idle"
elif self.mode == "dispensing":
if button == "refund": self.give_back() # bug: still dispensing
# "broken" mode? nobody remembered to handle it here
Build it one piece at a time
Step one: draw the modes as circles and the button presses as arrows between them on paper. This picture is a state machine. Write down for each mode which arrows exist; every pair with no arrow is an illegal event, and the program should refuse it rather than guess.
Step two: give each mode its own class. Each class has one method per event and decides what happens and which mode comes next. The machine itself keeps only the current mode object and the shared data, such as the balance.
class State:
def coin(self, m: "Machine") -> "State": raise IllegalEvent("coin")
def select(self, m: "Machine") -> "State": raise IllegalEvent("select")
def refund(self, m: "Machine") -> "State": raise IllegalEvent("refund")
class Idle(State):
def coin(self, m: "Machine") -> State:
m.balance += 10
return Paid()
class Paid(State):
def coin(self, m: "Machine") -> State:
m.balance += 10
return self
def select(self, m: "Machine") -> State:
m.drop()
return Dispensing()
def refund(self, m: "Machine") -> State:
m.give_back()
return Idle()
Step three: the machine forwards every event to the current state and stores whatever state comes back. The if-else wall is gone; the only check left is the one the state class makes by existing.
class Dispensing(State):
def done(self, m: "Machine") -> State:
m.balance = 0
return Idle()
class Machine:
def __init__(self) -> None:
self.state: State = Idle()
self.balance = 0
def coin(self) -> None: self.state = self.state.coin(self)
def select(self) -> None: self.state = self.state.select(self)
def refund(self) -> None: self.state = self.state.refund(self)
Pressing refund while dispensing now raises IllegalEvent from the base class, because Dispensing never defined a refund arrow. The bug could not be written.
Why it works
The principle: when behaviour depends on a mode, put each mode's behaviour in one place and make the transitions explicit. The invariant is that the machine is always in exactly one state, and every event either moves it along a drawn arrow or is rejected. Where it breaks: shared data that only makes sense in one state (a selected item while idle), transitions hidden inside helper methods so the diagram no longer matches the code, or an enum plus a dictionary of transitions when the states carry no behaviour, which is simpler and just as good. Interviewers test this by firing events in a wrong order and by asking you to add a new state; both should be a few lines.
The trade-off you must say out loud
Four modes become four classes, and a simple two-mode flag does not need any of this. The State pattern pays off when events are many and modes carry different logic; a transition table pays off when modes are many and logic is thin. The follow-up is almost always about concurrency: two threads firing events at once must not both read the old state, so the forwarding method takes a lock or events go through a queue.
Where it shows up in the 100 Google problems
- Vending machine: the example above, with inventory and change calculation added to the shared data.
- Elevator controller: idle, moving up, moving down, doors open; a hall call is an event whose effect depends on the state.
- Stopwatch: stopped, running and paused, with start, pause, resume and reset as the only legal arrows.
- Movie ticket booking: a seat goes free, held, booked, released; the hold expiring is an event too.
- Payment processor: created, authorised, captured, refunded, failed, with exactly-once transitions.
02Used in these problems
- Design a task scheduler with dependencies and retries
- Design a stopwatch class
- Design a customer-support agent dispatcher
- Design a path-value file system API
- Design an elevator controller
- Design a multi-elevator dispatcher
- Design a restaurant management system
- Design a movie ticket booking system
- Design a hotel booking system
- Design an airline reservation system
- Design a car rental system
- Design a library management system
- Design a course registration system
- Design a train platform management system
- Design chess
- Design Minesweeper
- Design an object-oriented poker game
- Design Monopoly
- Design an ATM
- Design a payment processor
- Design an online stock brokerage
- Design a vending machine
- Design a coffee machine
- Design an online shopping system
- Design a food delivery system
- Design an online auction
- Design a task management system (Trello-like)
- Design a traffic signal controller
- Design a music player