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?
- Why Use the State Pattern?
- State Pattern Comparison
- State Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
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:
- 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
- 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
- Single Responsibility: Each state class is responsible for one thing — the behavior of the context in that specific state
- Explicit State Transitions: Transitions are clearly defined inside state classes, making the full state machine easy to visualize and audit
- Testability: Each state class can be tested in complete isolation — no need to set up elaborate context conditions to reach a specific branch
- 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:
| Pattern | Purpose | Who Controls Flow | Behavior Change | Use Case |
|---|---|---|---|---|
| State | Change behavior based on internal state | State objects (and context) | Swapping state objects | Finite state machines, lifecycle objects |
| Strategy | Swap algorithms at runtime | Client/context | Swapping strategy objects | Sorting, compression, validation algorithms |
| Command | Encapsulate a request as an object | Invoker | Queuing/executing commands | Undo/redo, job queues, transactions |
| Chain of Responsibility | Pass a request along a handler chain | Handlers | Different handler processes request | Middleware, validation, escalation |
| Observer | Notify dependents of state changes | Subject | Observers react independently | Event 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:
-
Context: The object that has state-dependent behavior. It maintains a reference to the current
Stateobject. All client-facing methods delegate to the current state. The context provides a method for states to trigger transitions — oftensetState(newState). The context may also hold data that states need to do their work (e.g., a coin balance in a vending machine). -
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.
-
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()andonExit()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:
-
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.
-
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.
-
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
matchon 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
ABCand@abstractmethodfor 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
#privatefields on the context to prevent external state injection; keep concrete state classes unexported when possible - Java: Define a
VendingMachineStateinterface — avoid abstract classes with empty bodies (silent no-ops become bugs); useOptional<Product>for nullable product lookups; userecordfor immutableProductdata - JavaScript: Use
#privateclass fields for the state property — prevents external state bypassing; use module-levelconstsingletons 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; preferLinkedList<T>over arrays for histories if needed - PHP: Use
readonlyproperties for state metadata in PHP 8.1+; match expressions can supplement state delegation for exhaustiveness; document clearly that only the context should callsetState() - 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 withstd::mem::replace(the "take-and-replace" pattern) when states need mutable access to the context; deriveDebugon all state structs - Dart: Use
sealedclasses in Dart 3+ for exhaustiveswitchon states; use theabstract interface classkeyword for the state contract; single-instance states work well with Dart'sconstconstructors - Swift: Use
protocolfor the state interface, not a class; store state asany VendingMachineStateor use existentials;enumwith associated values is idiomatic for small FSMs in Swift — know when to use each - Kotlin: Use
objectdeclarations for stateless singleton states;sealed interfacefor a closed state hierarchy with exhaustivewhen;data classfor states that carry data; keep concrete state classesinternal
📋 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, Dartsealed, Rustenum) 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:
- 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
instanceofchecks - Keep transition logic in the states: The state that "knows" when a transition should happen should own the call to
setState() - Guard against invalid actions in each state: Decide per-state whether invalid actions are no-ops, messages, or exceptions — be consistent across your application
- 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
- 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 - Keep concrete state classes non-public: Clients should only interact with the context — never construct or inject states directly from outside the state machine
- Name states after what they represent, not what they do:
HasCoinStateis better thanCoinInsertedHandlerState— 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 inXhas 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()inHasCoinStateguards onmachine.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
lastActiveSubStatereference 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:
- Memento Pattern
- Observer Pattern
- Command Pattern
- Strategy Pattern
- Iterator Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.