bigonotes
CS ConceptsDesign Patterns

State Pattern

Model state machines like vending machines and document workflows with one class per state instead of if/else on a state field.

7 min readLast updated Design Patterns

A vending machine behaves differently when it's idle, when it has your money, while it's dispensing, and when it's sold out. If you track that with a state string, every method grows an if/elif over every state. State turns each state into its own class; the machine delegates to whichever state object is current, and the states decide when to switch. Many LLD interview problems (vending machine, elevator, order lifecycle, ATM) are really testing whether you know this pattern.

Intent#

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class. (GoF)

Analogy: water. Ice, liquid and steam are the same substance with completely different behavior, depending on state.

The problem: if/elif spaghetti#

Here is the vending machine with a plain state string:

class BadVendingMachine:
    def __init__(self):
        self.state = "IDLE"  # IDLE, HAS_MONEY, DISPENSING

    def insert_coin(self):
        if self.state == "IDLE":
            print("Coin accepted.")
            self.state = "HAS_MONEY"
        elif self.state == "HAS_MONEY":
            print("Already have a coin. Please select a product.")
        elif self.state == "DISPENSING":
            print("Please wait, dispensing in progress.")

    def select_product(self):
        if self.state == "IDLE":
            print("Insert coin first!")
        elif self.state == "HAS_MONEY":
            print("Product selected. Dispensing...")
            self.state = "DISPENSING"
            self.dispense()
        elif self.state == "DISPENSING":
            print("Already dispensing!")

    def dispense(self):
        if self.state == "DISPENSING":
            print("Item dropped. Thank you!")
            self.state = "IDLE"
        else:
            print("Cannot dispense right now.")
  • Unmaintainable: every method (insert_coin, select_product, cancel, ...) needs a branch for every state.
  • Violates Open/Closed: adding OUT_OF_STOCK means editing every method, which is exactly where bugs creep in.

Structure#

The pattern has three participants.

Participant Role
Context The object clients use (VendingMachine); holds the current state and delegates every call to it
State interface One method per action the context supports
Concrete states Implement behavior for one state and trigger transitions (context.set_state(...))

A single call flows like this:

  1. The client calls machine.insert_coin().
  2. The machine calls current_state.insert_coin(machine), passing itself.
  3. The state does the work, then calls machine.set_state(HasMoneyState()).

The context passes itself to the state so the state can call set_state() and read context data. That's the mind-bending part at first.

Example 1: vending machine#

Each state is a class, and the machine just forwards every action.

from abc import ABC, abstractmethod


class VendingMachineState(ABC):
    @abstractmethod
    def insert_coin(self, machine: "VendingMachine") -> None: ...

    @abstractmethod
    def select_product(self, machine: "VendingMachine") -> None: ...

    @abstractmethod
    def dispense(self, machine: "VendingMachine") -> None: ...


class IdleState(VendingMachineState):
    def insert_coin(self, machine):
        print("Coin accepted.")
        machine.set_state(HasMoneyState())  # transition

    def select_product(self, machine):
        print("Insert a coin first!")

    def dispense(self, machine):
        print("Insert a coin first!")


class HasMoneyState(VendingMachineState):
    def insert_coin(self, machine):
        print("Already have a coin. Select a product.")

    def select_product(self, machine):
        print("Product selected.")
        machine.set_state(DispensingState())
        machine.dispense()  # kick off the next step automatically

    def dispense(self, machine):
        print("Select a product first!")


class DispensingState(VendingMachineState):
    def insert_coin(self, machine):
        print("Please wait, dispensing...")

    def select_product(self, machine):
        print("Already dispensing!")

    def dispense(self, machine):
        machine.stock -= 1
        print(f"Item dropped. {machine.stock} left.")
        next_state = IdleState() if machine.stock > 0 else SoldOutState()
        machine.set_state(next_state)


class SoldOutState(VendingMachineState):
    def insert_coin(self, machine):
        print("Sold out. Coin returned.")

    def select_product(self, machine):
        print("Sold out.")

    def dispense(self, machine):
        print("Sold out.")


class VendingMachine:
    def __init__(self, stock: int):
        self.stock = stock
        self.state: VendingMachineState = (
            IdleState() if stock > 0 else SoldOutState()
        )

    def set_state(self, new_state: VendingMachineState) -> None:
        self.state = new_state

    # Every action is delegated to the current state
    def insert_coin(self) -> None:
        self.state.insert_coin(self)

    def select_product(self) -> None:
        self.state.select_product(self)

    def dispense(self) -> None:
        self.state.dispense(self)


machine = VendingMachine(stock=1)
machine.select_product()  # Insert a coin first!
machine.insert_coin()     # Coin accepted.
machine.select_product()  # Product selected. / Item dropped. 0 left.
machine.insert_coin()     # Sold out. Coin returned.

Adding SoldOutState touched only DispensingState (which decides when to go there) and the constructor. No other state changed.

Example 2: document workflow#

A document moves from draft to moderation to published, and can be sent back.

from abc import ABC, abstractmethod


class DocumentState(ABC):
    @abstractmethod
    def publish(self, doc: "Document") -> None: ...

    @abstractmethod
    def reject(self, doc: "Document") -> None: ...


class Document:
    def __init__(self, name: str):
        self.name = name
        self._state: DocumentState = DraftState()  # initial state

    def set_state(self, state: DocumentState) -> None:
        self._state = state

    def publish(self) -> None:
        self._state.publish(self)

    def reject(self) -> None:
        self._state.reject(self)


class DraftState(DocumentState):
    def publish(self, doc: Document) -> None:
        print(f"[{doc.name}] Draft → Moderation")
        doc.set_state(ModerationState())

    def reject(self, doc: Document) -> None:
        print(f"[{doc.name}] Can't reject a draft")


class ModerationState(DocumentState):
    def publish(self, doc: Document) -> None:
        print(f"[{doc.name}] Approved → Published")
        doc.set_state(PublishedState())

    def reject(self, doc: Document) -> None:
        print(f"[{doc.name}] Rejected → back to Draft")
        doc.set_state(DraftState())


class PublishedState(DocumentState):
    def publish(self, doc: Document) -> None:
        print(f"[{doc.name}] Already published")

    def reject(self, doc: Document) -> None:
        print(f"[{doc.name}] Can't reject a published document")


doc = Document("Report.pdf")
doc.publish()  # Draft → Moderation
doc.reject()   # Rejected → back to Draft
doc.publish()  # Draft → Moderation
doc.publish()  # Approved → Published
doc.publish()  # Already published

Example 3: cyclic states#

When every state has exactly one next state, each next() just moves on. A traffic light:

from abc import ABC, abstractmethod


class LightState(ABC):
    @abstractmethod
    def next(self, light: "TrafficLight") -> None: ...


class TrafficLight:
    def __init__(self):
        self._state: LightState = RedState()

    def set_state(self, state: LightState) -> None:
        self._state = state

    def change(self) -> None:
        self._state.next(self)


class RedState(LightState):
    def next(self, light: TrafficLight) -> None:
        print("Red: STOP")
        light.set_state(GreenState())


class GreenState(LightState):
    def next(self, light: TrafficLight) -> None:
        print("Green: GO")
        light.set_state(YellowState())


class YellowState(LightState):
    def next(self, light: TrafficLight) -> None:
        print("Yellow: SLOW")
        light.set_state(RedState())


light = TrafficLight()
for _ in range(4):
    light.change()  # Red, Green, Yellow, Red

A music player (Stopped, Playing, Paused) works the same way, with three actions instead of one: play() in Paused resumes, pause() in Stopped prints "can't pause", stop() from anywhere returns to Stopped.

Template#

The bare skeleton, ready to adapt:

from abc import ABC, abstractmethod


class State(ABC):
    @abstractmethod
    def handle(self, context: "Context") -> None: ...


class Context:
    def __init__(self):
        self._state: State = StateA()

    def set_state(self, state: State) -> None:
        self._state = state

    def request(self) -> None:
        self._state.handle(self)  # delegate


class StateA(State):
    def handle(self, context: Context) -> None:
        print("A handling; switching to B")
        context.set_state(StateB())  # the state controls the transition


class StateB(State):
    def handle(self, context: Context) -> None:
        print("B handling; switching to A")
        context.set_state(StateA())

Where you'll see state machines#

State machines are everywhere once you look for them.

  • Protocols: TCP connection states (Listen, Established, Closed), the textbook GoF example.
  • Workflows: loans, job applications, and the lifecycles below.
  • Devices: vending machines, ATMs (Idle, Card Inserted, PIN Entered), elevators, traffic lights, media players.
  • Games: character states (Idle, Running, Jumping, Attacking, Dead).
  • Libraries: packages such as transitions provide a framework for this in Python.
Workflow States, in order
Order Placed, Paid, Shipped, Delivered
Support ticket Open, In Progress, Resolved, Closed
Subscription Trial, Active, Suspended, Cancelled

When to use it#

State fits when:

  • An object's behavior depends heavily on its state, and it has several states.
  • Methods are full of conditionals on the same state field.
  • The states and transitions form a clear state machine that will likely grow.

When not to use it#

Skip it when:

  • Two or three states with trivial behavior. An Enum and a match statement are clearer.
  • The "state" is just data that doesn't change behavior.
  • A single boolean flag is enough.

Pitfalls#

Keep these in mind as the state machine grows.

  • Who owns transitions? Either the states decide (as above, flexible but spreads the transition map across classes) or the context does (centralized, but reintroduces conditionals). Pick one and stay consistent.
  • One class per state means more files; worth it only when the states carry real behavior.
  • Creating new state objects on every transition. Stateless state objects can be shared (made singletons or flyweights).
  • Forgetting invalid actions. Every state must handle every action, even if only to reject it. That's a feature: invalid transitions become explicit.

State vs similar patterns#

State shares its shape with several other patterns:

Pattern Difference
Strategy Same shape. A strategy is usually chosen by the client and rarely changes; states replace each other as the object runs
Command A command is an action ("play"); a state is a condition ("playing")
Bridge Bridge fixes the abstraction/implementation pairing up front; State swaps the implementation as state changes
Flyweight Often combined: share stateless state objects

Key takeaways#

  • One class per state; the context delegates every action to the current state object.
  • States trigger transitions by calling context.set_state(...), so the context passes itself in.
  • Adding a state means adding a class, not editing every method.
  • Every state handles every action, which makes invalid operations explicit.
  • Strategy and State share a structure; State is about the object changing its own behavior over time.

Found a mistake? Tell me on LinkedIn ↗

Further reading#