Behavioral Pattern

State

Allows an object to alter its behavior when its internal state changes.

State Pattern: A Complete Guide with Examples in 11 Programming Languages

The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The object will appear to change its class. Instead of scattering conditional logic (if/switch statements) throughout a class to handle different modes or phases, each state is encapsulated in its own class. The context object delegates behavior to the current state object. When the state changes, the context simply swaps the state object — and behavior changes automatically, without a single conditional in sight.

In this comprehensive guide, we'll explore the State Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.

Table of Contents

What is the State Pattern?

The State Pattern solves a classic problem in object-oriented design: how do you cleanly model an object whose behavior depends on its current mode, phase, or condition — without filling every method with tangled if/else or switch statements that grow unmanageable over time?

The naive approach — checking this.state === "LOCKED" in every method — doesn't scale. Add a new state and you must touch every method. Introduce a new action and you must check every state. The combinatorial explosion leads to brittle, hard-to-test code where a bug in one state accidentally affects another.

The State Pattern eliminates this by making each state a first-class object. The context holds a reference to the current state. Every action delegates to the state object. When the state changes, the context replaces the state object — and the new behavior kicks in immediately. Adding a new state means adding one new class, not modifying ten existing methods.

Think of it like a vending machine. The same button press does completely different things depending on whether the machine is idle, has money inserted, is dispensing, or is out of stock. Instead of one giant method that checks currentState in every branch, each mode is its own object that knows exactly what to do.

Why Use the State Pattern?

The State Pattern offers several compelling benefits:

  1. Eliminates Conditional Sprawl: Replace massive if/switch chains with a clean hierarchy of state classes — each containing only the logic relevant to its own state
  2. Open/Closed Principle: Add new states without modifying existing state classes or the context — new behavior is a new class, not a surgical edit to existing code
  3. Single Responsibility: Each state class is responsible for one thing — the behavior of the context in that specific state
  4. Explicit State Transitions: Transitions are clearly defined inside state classes, making the full state machine easy to visualize and audit
  5. Testability: Each state class can be tested in complete isolation — no need to set up elaborate context conditions to reach a specific branch
  6. Self-Documenting Code: The class hierarchy IS the state diagram — reading the code reveals the full lifecycle without reverse-engineering a tangle of conditions

State Pattern Comparison

Let's compare the State Pattern with related patterns:

PatternPurposeWho Controls FlowBehavior ChangeUse Case
StateChange behavior based on internal stateState objects (and context)Swapping state objectsFinite state machines, lifecycle objects
StrategySwap algorithms at runtimeClient/contextSwapping strategy objectsSorting, compression, validation algorithms
CommandEncapsulate a request as an objectInvokerQueuing/executing commandsUndo/redo, job queues, transactions
Chain of ResponsibilityPass a request along a handler chainHandlersDifferent handler processes requestMiddleware, validation, escalation
ObserverNotify dependents of state changesSubjectObservers react independentlyEvent systems, reactive UI

Key Distinctions:

  • State vs. Strategy: These look structurally identical — both delegate behavior to a swappable object. The difference is intent and ownership. A Strategy is set by the client and doesn't change autonomously. A State changes itself or is changed by transitions defined within the state hierarchy. A sorting algorithm is a Strategy. A traffic light phase is a State.
  • State vs. Finite State Machine (FSM): The State Pattern IS an object-oriented way to implement an FSM. An FSM describes states and transitions abstractly. The State Pattern implements those transitions as class-level behavior, with self-contained transition logic inside each state.
  • State vs. Conditional Logic: Conditional logic becomes unmanageable as states grow. State Pattern trades a flat if/switch chain for a class hierarchy. The hierarchy is more verbose for two states but dramatically cleaner for five or more.

State Pattern Explained

The State Pattern involves three participants:

Core Components:

  1. Context: The object that has state-dependent behavior. It maintains a reference to the current State object. All client-facing methods delegate to the current state. The context provides a method for states to trigger transitions — often setState(newState). The context may also hold data that states need to do their work (e.g., a coin balance in a vending machine).

  2. State Interface: Declares the methods that correspond to the context's state-dependent actions. Every concrete state implements this interface. Methods that are not meaningful in a given state can be left as no-ops or can throw errors — the choice is part of the state machine's design.

  3. Concrete States: Each concrete state class implements the behavior of the context in one specific state. States know which transitions are valid from their state and trigger them by calling context.setState(newState). States may hold a reference back to the context (passed via the action method or the constructor) to trigger transitions and read context data.

State Transition Strategies:

  • State-driven transitions: Each state decides when and how to transition — it calls context.setState(nextState) directly. The state "owns" its outgoing transitions. This is the most common approach and keeps transition logic close to the behavior that triggers it.
  • Context-driven transitions: The context manages all transitions via a transition table or explicit logic. States don't know about each other. Cleaner separation, but transition logic lives outside the states.
  • Event-driven transitions: An external event or message triggers a transition. Common in systems using event loops or message queues (e.g., UI frameworks, protocol parsers).

State Pattern Variants:

  • Singleton States: If states are stateless (no instance variables), they can be shared as singletons — one instance per state class rather than a new instance per context. Memory-efficient for large numbers of contexts.
  • State with Entry/Exit Actions: States define onEnter() and onExit() hooks that the context calls when transitioning. Useful for setup and teardown (logging, starting timers, sending notifications).
  • Hierarchical States: States can extend other states, inheriting default behavior for shared actions. A parent state provides fallback behavior; substates override only what they change.
  • History States: When re-entering a composite state, restore the last active substate rather than always starting from the initial substate. Common in UML state machines.

Class Diagram

Here's the UML class diagram showing the State Pattern structure:

Beginner-Friendly Example

Let's start with the most intuitive State Pattern example: a traffic light that cycles through Red, Green, and Yellow states. Each state knows its own behavior (how long to display, what to show) and which state comes next. The TrafficLight context delegates everything to the current state.

from __future__ import annotations
from abc import ABC, abstractmethod
import time


# ── State Interface ────────────────────────────────────────────────
class TrafficLightState(ABC):
    """Abstract base state — each concrete state implements these methods."""

    @abstractmethod
    def display(self) -> str:
        """Return what the light is showing."""

    @abstractmethod
    def next_state(self, light: "TrafficLight") -> None:
        """Transition to the appropriate next state."""

    @abstractmethod
    def duration(self) -> int:
        """How long (seconds) this state lasts."""


# ── Concrete States ────────────────────────────────────────────────
class RedState(TrafficLightState):
    def display(self) -> str:
        return "🔴 RED — Stop"

    def duration(self) -> int:
        return 30

    def next_state(self, light: "TrafficLight") -> None:
        print("  Red → Green")
        light.set_state(GreenState())


class GreenState(TrafficLightState):
    def display(self) -> str:
        return "🟢 GREEN — Go"

    def duration(self) -> int:
        return 25

    def next_state(self, light: "TrafficLight") -> None:
        print("  Green → Yellow")
        light.set_state(YellowState())


class YellowState(TrafficLightState):
    def display(self) -> str:
        return "🟡 YELLOW — Caution"

    def duration(self) -> int:
        return 5

    def next_state(self, light: "TrafficLight") -> None:
        print("  Yellow → Red")
        light.set_state(RedState())


# ── Context ─────────────────────────────────────────────────────────
class TrafficLight:
    """Context — delegates all behavior to the current state."""

    def __init__(self) -> None:
        self._state: TrafficLightState = RedState()  # Initial state

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

    def display(self) -> str:
        return self._state.display()

    def advance(self) -> None:
        """Advance to the next state."""
        self._state.next_state(self)

    def duration(self) -> int:
        return self._state.duration()


# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
    light = TrafficLight()

    print("=== Traffic Light Simulation ===\n")
    for _ in range(6):  # Two full cycles
        print(f"  {light.display()} (holds for {light.duration()}s)")
        light.advance()

Production-Ready Example

Now let's build a vending machine — a classic, richer application of the State Pattern. A vending machine has four states: Idle (waiting for a coin), HasCoin (coin inserted, waiting for selection), Dispensing (delivering a product), and OutOfStock (empty). Each action — insertCoin(), selectProduct(), dispense(), ejectCoin() — behaves differently depending on the current state.

from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional


# ── Product ─────────────────────────────────────────────────────────
@dataclass
class Product:
    name:  str
    price: int   # in cents


# ── State Interface ────────────────────────────────────────────────
class VendingMachineState(ABC):
    @abstractmethod
    def insert_coin(self, machine: "VendingMachine", amount: int) -> None: ...
    @abstractmethod
    def select_product(self, machine: "VendingMachine", product_id: str) -> None: ...
    @abstractmethod
    def dispense(self, machine: "VendingMachine") -> None: ...
    @abstractmethod
    def eject_coin(self, machine: "VendingMachine") -> None: ...
    @abstractmethod
    def name(self) -> str: ...


# ── Concrete States ────────────────────────────────────────────────
class IdleState(VendingMachineState):
    def name(self) -> str:
        return "IDLE"

    def insert_coin(self, machine: "VendingMachine", amount: int) -> None:
        machine.add_credit(amount)
        print(f"  💰 Coin inserted: {amount}¢. Credit: {machine.credit}¢")
        machine.set_state(HasCoinState())

    def select_product(self, machine: "VendingMachine", product_id: str) -> None:
        print("  ❌ Please insert a coin first.")

    def dispense(self, machine: "VendingMachine") -> None:
        print("  ❌ No product selected.")

    def eject_coin(self, machine: "VendingMachine") -> None:
        print("  ❌ No coin to eject.")


class HasCoinState(VendingMachineState):
    def name(self) -> str:
        return "HAS_COIN"

    def insert_coin(self, machine: "VendingMachine", amount: int) -> None:
        machine.add_credit(amount)
        print(f"  💰 Additional coin: {amount}¢. Total credit: {machine.credit}¢")

    def select_product(self, machine: "VendingMachine", product_id: str) -> None:
        product = machine.get_product(product_id)
        if product is None:
            print(f"  ❌ Product '{product_id}' not found.")
            return
        if machine.credit < product.price:
            shortfall = product.price - machine.credit
            print(f"  ❌ Insufficient credit. Need {shortfall}¢ more for {product.name}.")
            return
        machine.set_selected_product(product)
        print(f"  ✅ Selected: {product.name} ({product.price}¢). Dispensing…")
        machine.set_state(DispensingState())

    def dispense(self, machine: "VendingMachine") -> None:
        print("  ❌ Select a product first.")

    def eject_coin(self, machine: "VendingMachine") -> None:
        returned = machine.credit
        machine.reset_credit()
        machine.set_state(IdleState())
        print(f"  💸 Ejected {returned}¢. Returning to idle.")


class DispensingState(VendingMachineState):
    def name(self) -> str:
        return "DISPENSING"

    def insert_coin(self, machine: "VendingMachine", amount: int) -> None:
        print("  ⏳ Dispensing in progress. Coin returned.")
        machine.return_coins(amount)

    def select_product(self, machine: "VendingMachine", product_id: str) -> None:
        print("  ⏳ Already dispensing. Please wait.")

    def dispense(self, machine: "VendingMachine") -> None:
        product = machine.selected_product
        if product is None:
            return
        machine.deduct_credit(product.price)
        change = machine.credit
        machine.reset_credit()
        machine.remove_product(product)
        print(f"  🎁 Dispensed: {product.name}!")
        if change > 0:
            print(f"  💸 Change returned: {change}¢")
        next_state = OutOfStockState() if machine.is_empty() else IdleState()
        machine.set_state(next_state)

    def eject_coin(self, machine: "VendingMachine") -> None:
        print("  ❌ Cannot eject coin — dispensing in progress.")


class OutOfStockState(VendingMachineState):
    def name(self) -> str:
        return "OUT_OF_STOCK"

    def insert_coin(self, machine: "VendingMachine", amount: int) -> None:
        print(f"  ❌ Out of stock. Returning {amount}¢.")
        machine.return_coins(amount)

    def select_product(self, machine: "VendingMachine", product_id: str) -> None:
        print("  ❌ Out of stock. No products available.")

    def dispense(self, machine: "VendingMachine") -> None:
        print("  ❌ Nothing to dispense.")

    def eject_coin(self, machine: "VendingMachine") -> None:
        print("  ❌ No coin inserted.")


# ── Context ─────────────────────────────────────────────────────────
class VendingMachine:
    def __init__(self) -> None:
        self._state: VendingMachineState = IdleState()
        self._credit: int = 0
        self._selected: Optional[Product] = None
        self._inventory: dict[str, Product] = {
            "A1": Product("Cola",    150),
            "A2": Product("Water",    75),
            "A3": Product("Chips",   200),
        }

    # ── State delegation ──
    def insert_coin(self, amount: int) -> None:
        self._state.insert_coin(self, amount)

    def select_product(self, product_id: str) -> None:
        self._state.select_product(self, product_id)

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

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

    # ── State management ──
    def set_state(self, state: VendingMachineState) -> None:
        print(f"  [State: {self._state.name()}{state.name()}]")
        self._state = state

    # ── Credit management ──
    @property
    def credit(self) -> int:
        return self._credit

    def add_credit(self, amount: int) -> None:
        self._credit += amount

    def deduct_credit(self, amount: int) -> None:
        self._credit -= amount

    def reset_credit(self) -> None:
        self._credit = 0

    def return_coins(self, amount: int) -> None:
        print(f"  💸 Returned: {amount}¢")

    # ── Product management ──
    def get_product(self, product_id: str) -> Optional[Product]:
        return self._inventory.get(product_id)

    def set_selected_product(self, product: Product) -> None:
        self._selected = product

    @property
    def selected_product(self) -> Optional[Product]:
        return self._selected

    def remove_product(self, product: Product) -> None:
        for k, v in list(self._inventory.items()):
            if v is product:
                del self._inventory[k]
                self._selected = None
                return

    def is_empty(self) -> bool:
        return len(self._inventory) == 0

    def display_status(self) -> None:
        print(f"\n  ┌─ Vending Machine Status ─────────────")
        print(f"  │ State  : {self._state.name()}")
        print(f"  │ Credit : {self._credit}¢")
        print(f"  │ Stock  : {list(self._inventory.keys())}")
        print(f"  └──────────────────────────────────────\n")


# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
    machine = VendingMachine()
    machine.display_status()

    print("--- Select without coin ---")
    machine.select_product("A1")

    print("\n--- Insert coin and buy Cola ---")
    machine.insert_coin(100)
    machine.insert_coin(75)
    machine.select_product("A1")
    machine.dispense()
    machine.display_status()

    print("--- Insert coin and eject ---")
    machine.insert_coin(200)
    machine.eject_coin()
    machine.display_status()

    print("--- Buy remaining items to trigger OutOfStock ---")
    machine.insert_coin(100)
    machine.select_product("A2")
    machine.dispense()
    machine.insert_coin(200)
    machine.select_product("A3")
    machine.dispense()
    machine.display_status()

    print("--- Try to use empty machine ---")
    machine.insert_coin(100)

Real-World Use Cases

1. Vending Machines and ATMs

Every vending machine and ATM is a classic finite state machine. An ATM cycles through states like Idle, CardInserted, PINEntry, TransactionMenu, Dispensing, and OutOfService. Each button press delegates to the current state — pressing "Cancel" in PINEntry returns the card and goes back to Idle; pressing "Cancel" in TransactionMenu returns to PINEntry. Without the State Pattern, every button handler becomes an unmanageable nest of conditionals.

2. Order Management in E-commerce

An order passes through states: Pending, Confirmed, Processing, Shipped, Delivered, Cancelled, and Refunded. Each state allows only specific actions — a Delivered order cannot be Cancelled the same way a Pending order can. Each state class enforces its own valid transitions, preventing illegal state combinations and ensuring the business rules are co-located with the state they govern.

3. Media Player Controls

A media player has states like Stopped, Playing, Paused, and Buffering. Pressing "Play" starts playback from Stopped, resumes from Paused, is a no-op from Playing, and queues for later from Buffering. Each state handles the same user actions differently. The State Pattern means the UI layer just calls player.play() — it never needs to know what state the player is in.

4. Network Connection Lifecycle

A TCP connection cycles through Closed, Listen, SynSent, SynReceived, Established, FinWait1, FinWait2, CloseWait, Closing, LastAck, and TimeWait. Each state defines what packets are valid to send and receive, and which events trigger which transitions. Modeling this without the State Pattern means every packet handler becomes a conditional labyrinth — the State Pattern makes each phase a self-contained unit.

5. Game Character States

A game character has states like Idle, Running, Jumping, Falling, Attacking, Stunned, and Dead. Each state governs which animations play, which inputs are accepted, and what physics apply. In Stunned, the character ignores movement input. In Dead, all actions are ignored. The State Pattern allows each game state to cleanly define its behavior in isolation — a common pattern in game engines like Unity and Unreal.

6. Document Workflow (Draft → Review → Published)

A content management system models document states: Draft, InReview, Approved, Published, Archived. Only an editor can move a document from InReview to Approved. A Published document cannot go back to Draft. Each state defines which roles are allowed to trigger which transitions, and which actions are meaningful. Implementing this with conditionals throughout the CMS code creates a maintenance nightmare; the State Pattern encapsulates the rules per state.

7. UI Component States

A form submit button can be Enabled, Loading, Disabled, and Error. In Loading, it shows a spinner and ignores clicks. In Error, it shows an error icon and re-enables after a delay. In Disabled, all interaction is blocked. Modeling these with if (isLoading) else if (isError) throughout the component leads to bugs when new states are added. The State Pattern makes each UI mode explicit and testable.


Language-Specific Mistakes and Anti-Patterns

❌ Mistake #1: Using a String Flag Instead of State Objects

# BAD: A string flag leads to conditional sprawl across every method
class VendingMachine:
    def __init__(self):
        self.state = "idle"  # String flag — not a State object

    def insert_coin(self, amount):
        if self.state == "idle":
            self.credit += amount
            self.state = "has_coin"
        elif self.state == "has_coin":
            self.credit += amount
        elif self.state == "out_of_stock":
            print("Out of stock — returning coin")
        # Every method repeats this same conditional structure!

    def select_product(self, product_id):
        if self.state == "idle":
            print("Insert coin first")
        elif self.state == "has_coin":
            # ... selection logic
            self.state = "dispensing"
        elif self.state == "out_of_stock":
            print("Out of stock")
        # Adding a new state means editing every single method

# GOOD: Each state is a class with its own insert_coin / select_product
class VendingMachine:
    def __init__(self):
        self._state = IdleState()  # Swap the object, not a flag

    def insert_coin(self, amount):
        self._state.insert_coin(self, amount)  # Delegate — no conditionals

❌ Mistake #2: Forgetting to Pass the Context to State Methods

# BAD: State methods have no way to trigger transitions or read context data
class HasCoinState:
    def select_product(self, product_id: str) -> None:
        # How do we read machine.credit? How do we call machine.set_state()?
        print("Selected:", product_id)
        # Can't transition — self.machine doesn't exist!

# GOOD: Always pass the context object so states can trigger transitions
class HasCoinState:
    def select_product(self, machine: VendingMachine, product_id: str) -> None:
        if machine.credit >= machine.get_product(product_id).price:
            machine.set_selected_product(machine.get_product(product_id))
            machine.set_state(DispensingState())  # Context reference enables this

❌ Mistake #3: Putting Transition Logic in the Context Instead of the States

# BAD: Context decides all transitions — states are just dumb data holders
class VendingMachine:
    def insert_coin(self, amount):
        self._state.insert_coin(amount)
        # Context decides the transition — coupling context to every state type
        if isinstance(self._state, IdleState):
            self.set_state(HasCoinState())
        elif isinstance(self._state, OutOfStockState):
            self.return_coins(amount)

# GOOD: Let each state decide its own transitions
class IdleState:
    def insert_coin(self, machine, amount):
        machine.add_credit(amount)
        machine.set_state(HasCoinState())  # State owns this transition

Frequently Asked Questions

Q1: What is the difference between the State Pattern and the Strategy Pattern?

They look structurally identical — both delegate behavior to a swappable object — but they differ in intent, ownership, and autonomy.

Strategy represents an interchangeable algorithm chosen by the client. The client picks and assigns the strategy. The strategy itself never changes its own assignment. Example: choosing a sorting algorithm or a payment method. Strategies don't know about each other and don't self-transition.

State represents a lifecycle phase of the context. States transition themselves (or the context transitions them based on state-defined rules). States often know about each other — an IdleState knows it should transition to HasCoinState after a coin is inserted. The context's behavior emerges from the current state, not from an external client assignment.

A good heuristic: if the "strategy" changes autonomously based on conditions the object controls, it's a State. If the "strategy" is always assigned externally and never self-changes, it's a Strategy.

Q2: Should state transitions happen inside the state objects or inside the context?

Both approaches are valid and each has trade-offs.

State-driven transitions (transitions inside state classes): Each state knows which state comes next and calls context.setState(nextState). This keeps transition logic close to the conditions that trigger it — the HasCoinState is the natural place to decide that a successful product selection transitions to DispensingState. This is the most common approach and the one used throughout this guide.

Context-driven transitions (transition table in context): The context maintains an explicit transition map and decides all state changes. States signal events, and the context looks up the next state in the table. This is cleaner from a coupling standpoint (states don't reference each other) but the transition logic is separated from the behavior that triggers it, making the full FSM harder to see at a glance.

For most real-world applications, state-driven transitions are simpler and more maintainable.

Q3: How do I handle an action that is invalid in the current state?

Three common strategies:

  1. Silent no-op: The state method does nothing. Use this when the action is physically impossible to trigger in a well-designed UI (the button is grayed out). Silence is acceptable when the caller has already prevented the invalid action.

  2. Informative message: Log a warning or print a message. Use this during development or for debugging. For vending machines and UIs, this is the most user-friendly approach.

  3. Throw an exception: Use this when an invalid action indicates a programming error — something in the code called the action despite the known state. throw IllegalStateException("Cannot dispense in IDLE state"). Appropriate for protocol implementations and state machines where violations are bugs, not user errors.

The choice depends on whether the invalid action is a user mistake (handle gracefully) or a programming error (throw).

Q4: How do I use singleton states for memory efficiency?

Singleton states work when the state classes hold no instance-specific data — they're pure behavior containers. Check whether removing all instance variables from a state class leaves it functionally identical. If yes, it's a singleton candidate.

# Python: Module-level singleton instances
_IDLE_STATE     = IdleState()
_HAS_COIN_STATE = HasCoinState()
_OUT_OF_STOCK   = OutOfStockState()

class IdleState(VendingMachineState):
    def insert_coin(self, machine, amount):
        machine.add_credit(amount)
        machine.set_state(_HAS_COIN_STATE)  # Reuse singleton

# If DispensingState holds no instance data, it can be a singleton too

In Kotlin, object declarations are built-in singletons. In Java, use a static INSTANCE field or an enum. In Python, use module-level instances. Be careful: if a state class ever needs to store per-context data, singletons break — use per-context instances instead.

Q5: How do I add entry and exit actions to states?

Entry and exit hooks allow states to perform setup and teardown automatically when the context transitions in or out of them. Implement them in the context's setState() method:

class VendingMachine:
    def set_state(self, new_state: VendingMachineState) -> None:
        if self._state is not new_state:
            if hasattr(self._state, 'on_exit'):
                self._state.on_exit(self)   # Notify outgoing state
            self._state = new_state
            if hasattr(self._state, 'on_enter'):
                self._state.on_enter(self)  # Notify incoming state

class DispensingState(VendingMachineState):
    def on_enter(self, machine: VendingMachine) -> None:
        print("  [Dispensing motor starting…]")
        # Start timer, play sound, update UI

    def on_exit(self, machine: VendingMachine) -> None:
        print("  [Dispensing motor stopping]")

Entry/exit hooks are particularly useful for animations, timers, logging, and UI updates.

Q6: How do I implement a hierarchical state machine (parent/child states)?

In a hierarchical FSM, a parent state provides default behavior that child states inherit and override only as needed. This is useful when many states share common behavior.

class BaseMenuState(VendingMachineState):
    """Parent state: all menu states share cancel behavior."""
    def eject_coin(self, machine):
        machine.reset_credit()
        machine.set_state(IdleState())
        print("  💸 Transaction cancelled — coin returned.")

    def insert_coin(self, machine, amount):
        machine.add_credit(amount)
        print(f"  💰 Added {amount}¢. Total: {machine.credit}¢")

class SelectionMenuState(BaseMenuState):
    """Child state: inherits cancel/add-coin, overrides product selection."""
    def select_product(self, machine, product_id):
        # ... custom selection logic
        pass

Q7: When is a simple if/switch better than the State Pattern?

The State Pattern adds complexity through indirection — multiple classes, cross-references, and delegation. It pays off when the complexity of the state machine justifies it. Prefer simple conditionals when:

You have 2-3 states with 1-2 actions: A boolean flag and a single if statement is genuinely simpler. The State Pattern's overhead isn't worth it.

States and actions are stable and unlikely to grow: If the state machine is complete and you're confident it won't change, a switch statement is easier to read and maintain.

You're using a language with excellent pattern matching: Languages like Rust, Swift, Kotlin, and Dart offer exhaustive match/when/switch expressions on sealed types. For small, stable FSMs, exhaustive pattern matching provides the same safety guarantees with less boilerplate.

Use the State Pattern when you have 4+ states, multiple actions per state, or expect the state machine to grow over time.

Q8: How do I test state-based objects?

Each concrete state class can be tested in complete isolation:

def test_has_coin_state_ejects_coin():
    machine = VendingMachine()
    machine._state = HasCoinState()  # Force the state for testing
    machine._credit = 150

    machine.eject_coin()

    assert machine._credit == 0
    assert isinstance(machine._state, IdleState)

def test_out_of_stock_returns_coin_on_insert():
    machine = VendingMachine()
    machine._state = OutOfStockState()
    returned_coins = []

    machine.return_coins = lambda amount: returned_coins.append(amount)  # Mock
    machine.insert_coin(100)

    assert returned_coins == [100]  # Coin was returned
    assert isinstance(machine._state, OutOfStockState)  # State unchanged

The key advantage of the State Pattern for testing: you can directly inject any state without having to simulate the sequence of actions that would naturally get the context there. Each state is a unit; test it as a unit.

Q9: Is the State Pattern the same as a Finite State Machine?

Yes and no. A Finite State Machine (FSM) is a mathematical model — a set of states, an alphabet of events, a transition function, an initial state, and a set of accepting states. The State Pattern is one object-oriented implementation strategy for an FSM.

Other FSM implementations include:

  • Transition table: A 2D array indexed by [current_state][event] → next state. Compact and data-driven but hard to attach behavior to transitions.
  • Enum + switch: Simple but doesn't scale and violates Open/Closed.
  • State chart libraries: Tools like XState (JavaScript) or statecharts (UML) implement hierarchical FSMs with guards, history, and parallel states — more powerful than the basic State Pattern.

The State Pattern is the idiomatic OOP approach. It's the right choice when states have complex, method-rich behavior. It's overkill when the FSM is simple and data-driven (a transition table would suffice).

Q10: When should I NOT use the State Pattern?

Avoid the State Pattern when:

  • You have only 2-3 states and they're stable: A simple flag or boolean is more readable for a trivially small FSM.
  • States share so much logic that the classes become nearly identical: If every state does 95% the same thing, a template method or Strategy might be cleaner.
  • All state transitions are always decided by the client, never by the object itself: If the caller always decides the state, you have a Strategy, not a State.
  • You're in a language with excellent exhaustive pattern matching: For small, stable FSMs in Rust/Kotlin/Swift/Dart, exhaustive match on a sealed type is idiomatic and safe without the overhead of a class hierarchy.
  • Performance is critical and object allocation is a bottleneck: In tight inner loops, allocating state objects on every transition can matter. Use singletons or preallocate all states.

Key Takeaways

🎯 Core Concept: The State Pattern models an object whose behavior changes based on its current internal state. Instead of embedding conditional logic in every method, each state is its own class that encapsulates the behavior and transitions relevant to that phase. The context delegates all actions to the current state object and swaps the state object when transitioning — behavior changes without a single if/switch in the context.

🔑 Key Benefits:

  • No conditional sprawl: Replace if/switch chains with clean class delegation — each state class contains only what it needs
  • Open/Closed Principle: Add new states by adding new classes, not by modifying existing ones
  • Testability: Each state is isolated and directly injectable — test states as pure units
  • Self-documenting FSM: The class hierarchy IS the state diagram — the code reveals the full lifecycle
  • Encapsulated transitions: Transition rules live in the states that own them, not scattered through the context

🌐 Language-Specific Highlights:

  • Python: Use ABC and @abstractmethod for the state interface; pass context as the first argument to every state method; use module-level singletons for stateless states
  • TypeScript: Define a proper interface, not a class base; use #private fields on the context to prevent external state injection; keep concrete state classes unexported when possible
  • Java: Define a VendingMachineState interface — avoid abstract classes with empty bodies (silent no-ops become bugs); use Optional<Product> for nullable product lookups; use record for immutable Product data
  • JavaScript: Use #private class fields for the state property — prevents external state bypassing; use module-level const singletons for stateless states; avoid boolean flag pyramids
  • C#: Keep concrete state classes internal sealed — clients should never construct states directly; use an interface, not a base class; prefer LinkedList<T> over arrays for histories if needed
  • PHP: Use readonly properties for state metadata in PHP 8.1+; match expressions can supplement state delegation for exhaustiveness; document clearly that only the context should call setState()
  • Go: Always store state as an interface, never a struct value; unexported struct fields prevent external construction; use pointer receivers for state methods that modify the machine
  • Rust: Use Box<dyn Trait> for the state field; handle the borrow checker with std::mem::replace (the "take-and-replace" pattern) when states need mutable access to the context; derive Debug on all state structs
  • Dart: Use sealed classes in Dart 3+ for exhaustive switch on states; use the abstract interface class keyword for the state contract; single-instance states work well with Dart's const constructors
  • Swift: Use protocol for the state interface, not a class; store state as any VendingMachineState or use existentials; enum with associated values is idiomatic for small FSMs in Swift — know when to use each
  • Kotlin: Use object declarations for stateless singleton states; sealed interface for a closed state hierarchy with exhaustive when; data class for states that carry data; keep concrete state classes internal

📋 When to Use State:

  • An object's behavior changes substantially based on its current mode, phase, or lifecycle stage
  • You have 4+ states and multiple actions, and the state machine is expected to grow
  • Conditional logic per method is becoming complex and hard to test
  • You want to enforce valid transitions — invalid actions in a given state should be handled cleanly
  • You need entry/exit hooks, logging, or analytics on state changes

⚠️ When NOT to Use State:

  • Only 2-3 stable states — a simple flag or boolean is clearer
  • All transitions are always decided externally by the client — use Strategy instead
  • The object has state but the behavior doesn't change meaningfully across states
  • Language's pattern matching on sealed types (Kotlin sealed, Dart sealed, Rust enum) offers a cleaner, exhaustive solution for a simple, stable FSM
  • Performance-critical paths where object allocation per transition is measurable overhead

🛠️ Best Practices Across Languages:

  1. Define a strict state interface: Every action the context supports should be a method on the state interface — leaving any out leads to silent failures or instanceof checks
  2. Keep transition logic in the states: The state that "knows" when a transition should happen should own the call to setState()
  3. Guard against invalid actions in each state: Decide per-state whether invalid actions are no-ops, messages, or exceptions — be consistent across your application
  4. Use singleton states for stateless state classes: If a state holds no instance data, one shared instance suffices — no need to allocate on every transition
  5. Add entry/exit hooks via the context's setState(): Centralizing the hook calls in one place ensures they're never missed on any transition path
  6. Keep concrete state classes non-public: Clients should only interact with the context — never construct or inject states directly from outside the state machine
  7. Name states after what they represent, not what they do: HasCoinState is better than CoinInsertedHandlerState — the name should describe the phase, not the trigger

💡 Common Pitfalls:

  • String/enum flags replacing state objects: The most common anti-pattern — every method becomes a conditional maze that must be updated for every new state
  • Transition logic in the context: When the context checks if current_state is X then go to Y, the logic that belongs in X has leaked — transitions should live in states
  • Stateless base class with no-op methods: An abstract base class with empty method bodies creates silent bugs when a state forgets to override an action — use an interface that enforces all methods
  • States referencing each other's internals: States should only know the next state's type, not its implementation — machine.setState(new IdleState()) is fine; machine.setState(new IdleState(someInternalData)) is a smell
  • Forgetting to handle every action in every state: Even if an action is invalid in a state, it should be explicitly handled — an unimplemented method silently falls through

🎁 Advanced Techniques:

  • State + Observer hybrid: States fire events through the context's observer list when transitioning — UI layers subscribe and update automatically without tight coupling to state classes
  • Persistent state machines: Serialize the current state name alongside the context's data to disk or a database. On load, reconstruct the context in the correct state by name.
  • Guard conditions: Before transitioning, evaluate a condition that must be true. selectProduct() in HasCoinState guards on machine.credit >= product.price — the transition only fires if the guard passes.
  • History state: When re-entering a composite state, restore the last active substate rather than starting fresh. Store a lastActiveSubState reference in the parent state.
  • Parallel states: Multiple independent state machines running simultaneously on the same object — a media player can be in [Playing, FullScreen] or [Paused, Windowed] where audio and display states are independent orthogonal regions.
  • State machine generators: For complex FSMs with many states, use a declarative DSL or library (XState, Stateless.NET, state_machine gem) that generates the class hierarchy from a transition table — reduces boilerplate and makes the FSM diagram explicit in code.

The State Pattern is the cleanest object-oriented solution whenever an object's behavior is fundamentally governed by its current phase. It transforms a sprawling conditional tangle into a structured, testable hierarchy where each phase of the object's life is a first-class citizen with its own identity, behavior, and transition rules.


Want to dive deeper into design patterns? Check out our comprehensive guides on: