Design Patterns: The State Pattern (GoF)
The State pattern is one of the behavioral patterns described in Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994). It allows an object to alter its behavior when its internal state changes, making it appear as if the object has changed its class.
What is the State pattern?
State encapsulates the behavior associated with each state into separate classes,
delegating the context’s operations to the current state instance. It is an alternative to large conditional
structures (if-else or switch) that become unmaintainable as the number of states grows.
It is related to finite state machines (FSM), but applied in an object-oriented way, improving extensibility and adherence to SOLID principles (especially OCP and SRP).
When should you use the State pattern?
Apply it when:
- Behavior depends heavily on internal state and changes significantly between states.
- You have conditionals everywhere checking state.
- You need to add states frequently without modifying the existing Context code.
- You want to clearly model transitions and associated actions (entry, exit, do).
Classic examples: media players (Playing/Paused/Stopped), TCP connections (Established/Listening/Closed), e-commerce orders (New/Processing/Shipped/Delivered/Canceled), or a character’s states in a video game.
State pattern structure (according to GoF)
The State pattern is composed of three main elements:
Context
- state: State
+ request()
<<interface>> State
+ handle()
ConcreteStateA
+ handle() (changes context.state)
ConcreteStateB
+ handle() (changes context.state)
Main components
- Context: holds a reference to the current state and delegates requests.
- State: interface/abstract class that defines the states API.
- ConcreteState: implements state-specific behavior and manages transitions.
Pros and cons
Pros
- Open-Closed: add states by creating new classes without touching the Context.
- Single Responsibility: each state has its own well-scoped logic.
- Eliminates conditionals: avoids huge switch/if-else blocks.
- Clarity: the model reflects the domain’s state diagram.
- Testing: you can test each state in isolation.
Cons and considerations
- Increases the number of classes (one per state).
- Slight overhead in memory/runtime.
- With few states and simple transitions, it can be overengineering (enum + switch might be enough).
Practical example in Python
Implementation of a player with three states: Stopped, Playing, and Paused.
from abc import ABC, abstractmethod
# State interface
class PlayerState(ABC):
@abstractmethod
def play(self, player): ...
@abstractmethod
def pause(self, player): ...
@abstractmethod
def stop(self, player): ...
# Concrete states
class StoppedState(PlayerState):
def play(self, player):
print("Starting playback...")
player.set_state(PlayingState())
def pause(self, player):
print("Cannot pause: it is already stopped.")
def stop(self, player):
print("Already stopped.")
class PlayingState(PlayerState):
def play(self, player):
print("Already playing.")
def pause(self, player):
print("Pausing playback...")
player.set_state(PausedState())
def stop(self, player):
print("Stopping playback...")
player.set_state(StoppedState())
class PausedState(PlayerState):
def play(self, player):
print("Resuming playback...")
player.set_state(PlayingState())
def pause(self, player):
print("Already paused.")
def stop(self, player):
print("Stopping from paused...")
player.set_state(StoppedState())
# Context
class MediaPlayer:
def __init__(self):
self._state = StoppedState() # Initial state
def set_state(self, state: PlayerState):
self._state = state
def play(self):
self._state.play(self)
def pause(self):
self._state.pause(self)
def stop(self):
self._state.stop(self)
# Usage
player = MediaPlayer()
player.play() # Starting playback...
player.pause() # Pausing playback...
player.play() # Resuming playback...
player.stop() # Stopping playback...
player.pause() # Cannot pause: it is already stopped.
State vs alternatives
Practical rule
If your object lives in an endless if/elif/else and every feature adds another branch,
State isn’t overengineering: it’s damage control.
State vs Strategy
- Strategy changes how an algorithm is executed (same goal, different implementations).
- State changes what you’re allowed to do and the overall behavior based on the state.
- In State, there are usually transitions (an action may change the state).
State vs enum + switch
- enum + switch starts fast, but scales poorly as states and rules grow.
- State distributes complexity: each state encapsulates behavior + validations + transitions.
- With 2–3 simple states that hardly change, enum+switch may be enough.
Decision checklist
- Different rules per state? → yes
- Transitions with validations (“only if…”) ? → yes
- States added frequently? → yes
- Your logic today is a giant conditional? → yes
- If you checked 2 or more: State usually pays off.
Transitions and on_enter / on_exit actions
In real systems, it’s not enough to “change behavior”. You need to control transitions and execute actions on entry/exit (logs, metrics, events, auditing).
from abc import ABC
class PlayerState(ABC):
def on_enter(self, player): ...
def on_exit(self, player): ...
class PlayingState(PlayerState):
def on_enter(self, player):
player.metrics["starts"] += 1
class MediaPlayer:
def __init__(self):
self.metrics = {"starts": 0}
self._state = None
def set_state(self, state: PlayerState):
# exit current state
if self._state and hasattr(self._state, "on_exit"):
self._state.on_exit(self)
self._state = state
# enter new state
if hasattr(self._state, "on_enter"):
self._state.on_enter(self)
Realistic example: an order lifecycle (e-commerce)
A typical order: New → Paid → Shipped → Delivered, with an exit path like Canceled. Each state defines which actions are allowed.
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def pay(self, order): ...
@abstractmethod
def ship(self, order): ...
@abstractmethod
def deliver(self, order): ...
@abstractmethod
def cancel(self, order): ...
class New(OrderState):
def pay(self, order):
order.set_state(Paid())
def ship(self, order):
raise ValueError("You can’t ship: payment is missing.")
def deliver(self, order):
raise ValueError("You can’t deliver: it hasn’t been shipped.")
def cancel(self, order):
order.set_state(Canceled())
class Paid(OrderState):
def pay(self, order):
raise ValueError("Already paid.")
def ship(self, order):
order.set_state(Shipped())
def deliver(self, order):
raise ValueError("You can’t deliver: it hasn’t been shipped.")
def cancel(self, order):
order.set_state(Canceled())
class Shipped(OrderState):
def pay(self, order): raise ValueError("Not applicable.")
def ship(self, order): raise ValueError("Already shipped.")
def deliver(self, order):
order.set_state(Delivered())
def cancel(self, order):
raise ValueError("You can’t cancel: it’s already shipped.")
class Delivered(OrderState):
def pay(self, order): raise ValueError("Not applicable.")
def ship(self, order): raise ValueError("Not applicable.")
def deliver(self, order): raise ValueError("Already delivered.")
def cancel(self, order): raise ValueError("You can’t cancel: already delivered.")
class Canceled(OrderState):
def pay(self, order): raise ValueError("Order canceled.")
def ship(self, order): raise ValueError("Order canceled.")
def deliver(self, order): raise ValueError("Order canceled.")
def cancel(self, order): raise ValueError("Already canceled.")
class Order:
def __init__(self):
self._state = New()
self.history = ["New"]
def set_state(self, state: OrderState):
self._state = state
self.history.append(state.__class__.__name__)
def pay(self): self._state.pay(self)
def ship(self): self._state.ship(self)
def deliver(self): self._state.deliver(self)
def cancel(self): self._state.cancel(self)
o = Order()
o.pay()
o.ship()
o.deliver()
print(o.history)
Persistence tip (DB)
Store the state as a string/enum (e.g., "Paid") and resolve it with a factory when loading.
This avoids serializing objects and simplifies auditing and migrations.
Professional variants of the pattern
Shared states (Singleton/Flyweight)
If states are “pure” (no per-instance data), you can instantiate them once and reuse them.
Centralized vs distributed transitions
- Distributed: each
ConcreteStatedecides which state to transition to (more OO). - Centralized: the Context keeps a transition table (useful if the graph is large).
Events instead of methods
In complex domains, you can use dispatch(event) instead of play()/pause().
It improves logging, metrics, and testing.
Common mistakes
- The Context goes back to if/else per state: it “undoes” the pattern.
- Huge State interface: keep only methods that actually vary by state.
- Transitions without traceability: store history or logs.
Quick per-state testing
import pytest
def test_new_cannot_ship():
o = Order()
with pytest.raises(ValueError):
o.ship()
def test_paid_can_ship():
o = Order()
o.pay()
o.ship()
assert o.history[-1] == "Shipped"
Relationship with hierarchical state machines (HSM)
The State pattern is a natural foundation for HSMs: states can have substates,
or you can implement a “superstate” via composition. In a media player you could extend PlayingState
with substates like “Buffering” or “Streaming”.
- States that reference substates.
- Composition for superstates.
- Frameworks: XState (JS), Stateless (C#), Spring StateMachine (Java), etc.
Professional best practices
- A State interface with methods that truly vary by state.
- Transitions centralized in ConcreteState (or a separate table if it grows too much).
- on_enter/on_exit actions if you need hooks.
- Combine with Strategy (algorithms inside a state) or Flyweight (many similar states).
Conclusion
The State pattern models state-dependent behavior in a clean and extensible way. It turns fragile conditionals into an object-oriented structure that scales better as states and transitions grow.
Related patterns
60-second summary
- State encapsulates behavior per state and reduces conditionals.
- It shines when states, transitions, and rules grow.
- If there are few simple states: enum+switch may be enough.