InterviewKata
Design patterns · 14 of 26 · Choosing behaviour

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

A traffic light doesn't check a list of switches to decide what "next" means. When it's red, next is green; when it's green, next is amber. Each colour knows its own rules and which colour comes after it. Add a flashing-amber mode and you describe that mode alone.

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

RoleIts jobIn Pedal
ContextHolds the current state and passes each event to it.Bike with handle(event)
StateThe interface: one method per event.BikeState: unlock(), give_back(), report_fault()…
Concrete stateThe 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 = True

With 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 changeWithout the patternWith it
Add a "reserved" stateA fourth boolean, checked in every methodA Reserved class, and Docked gains reserve()
Allow returning a faulty bikeFind every check of faultyAdd give_back() to Faulty
Explain the rules to a new engineerRead five methodsRead 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 events13 Strategy
    There are two states and simple rulesA boolean or an enum with a match
    The machine is large, data-driven or edited by non-programmersA transition table (state × event → next state)
    States must survive restartsStore the state name; rebuild the object on load

    8. How it goes wrong

    States that set the context's fields directlyIf InUse reaches into Bike to flip flags, the flags are back. Return the next state instead.
    Forgetting the default refusalIf the base class silently ignores unknown events, a missing method looks like success. Refuse with a reason.
    Transitions hidden in the contextAn if state is Docked and event == … in Bike puts the switch statement back.
    A new object per transition when states hold no dataStateless states can be shared single instances; allocate only when a state carries data.

    9. Remember it and say it

    Memory hook: "The light knows what's next"Each state knows its own rules and its successor; the object just asks the current one.
    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 lightThe code
    Red, amber, greenDocked, InUse, Faulty, InRepair
    "Change" buttonAn event such as unlock
    Red refuses to go straight to amberFaulty refuses unlock
    Adding flashing amberAdding Reserved

    Watch it on an example

    1. B-17 is docked: unlock moves it to in use.
    2. A fault is reported: it becomes faulty.
    3. Someone tries to unlock it: Faulty refuses, "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

    10. Check yourself

    What does the context do with an event?

    The state decides what happens next.

    A faulty bike gets unlock. Who refuses?

    Faulty doesn't define unlock, so the base class refuses with its reason.

    What bug class does State remove?

    One state at a time.

    State vs strategy: who changes the delegate?

    Strategies are swapped from outside.

    Adding a Reserved state touches…

    Docked gains reserve(); Reserved handles the rest.

    11. Go deeper

    ResourceWhy
    13 StrategySame structure; chosen from outside.
    Low-level design trackVending machines, elevators and parking lots: state machines in interview problems.
    Python enumNaming states when a full pattern is too much.
    Wikipedia: State patternStructure and relation to finite-state machines.