1. What Is It?
The pattern allows an object to alter its behavior when its internal changes. The object will appear to change its class. Each state is encapsulated in its own class, and the context delegates behavior to the current state object.
Without it, state-dependent behavior ends up as a tangle of if/elif/switch blocks inside a single class — every method that cares about state must check it, every new state requires editing every method. The class grows proportionally to states × transitions × behaviors. The State pattern externalizes each state into its own class, making transitions explicit and behavior self-contained.
A media player class handles playback differently depending on whether it is in a Playing, Paused, or Stopped state. Currently, every method like play(), pause(), and stop() begins with a series of if/elif checks against a current_state variable. A fourth state, Buffering, needs to be added. What is the primary maintenance problem with this approach?
2. How It Works
Participants:
- — declaring the behavior methods that vary by
- ConcreteState — implements behavior for a specific state; may also trigger state transitions by calling
context.set_state() - Context — holds a reference to the current State object; delegates method calls to it; exposes
set_state()for transitions
Step-by-step:
- Identify the state-dependent behaviors (the methods that have
if state == X: ...logic) - Extract a
with those methods - Create a
ConcreteStateclass for each state — each implements all State methods with the right behavior for that state - Context delegates to
current_state.method()— no conditionals needed - Transitions: when a state's method determines the next state, it calls
context.set_state(NextState())
Python:
from __future__ import annotations
from abc import ABC, abstractmethod
class VendingMachineState(ABC):
@abstractmethod
def insert_coin(self, context: VendingMachine) -> None:
pass
@abstractmethod
def select_item(self, context: VendingMachine) -> None:
pass
@abstractmethod
def dispense(self, context: VendingMachine) -> None:
pass
class IdleState(VendingMachineState):
def insert_coin(self, context: VendingMachine) -> None:
print("Coin inserted.")
context.set_state(CoinInsertedState())
def select_item(self, context: VendingMachine) -> None:
print("Please insert a coin first.")
def dispense(self, context: VendingMachine) -> None:
print("Please insert a coin first.")
class CoinInsertedState(VendingMachineState):
def insert_coin(self, context: VendingMachine) -> None:
print("Coin already inserted.")
def select_item(self, context: VendingMachine) -> None:
print("Item selected.")
context.set_state(ItemSelectedState())
def dispense(self, context: VendingMachine) -> None:
print("Please select an item first.")
class ItemSelectedState(VendingMachineState):
def insert_coin(self, context: VendingMachine) -> None:
print("Please wait — dispensing in progress.")
def select_item(self, context: VendingMachine) -> None:
print("Item already selected.")
def dispense(self, context: VendingMachine) -> None:
print("Dispensing item. Enjoy!")
context.set_state(IdleState())
class VendingMachine:
def __init__(self) -> None:
self._state: VendingMachineState = IdleState()
def set_state(self, state: VendingMachineState) -> None:
self._state = state
def insert_coin(self) -> None:
self._state.insert_coin(self)
def select_item(self) -> None:
self._state.select_item(self)
def dispense(self) -> None:
self._state.dispense(self)
# Usage
machine = VendingMachine()
machine.select_item() # Please insert a coin first.
machine.insert_coin() # Coin inserted.
machine.insert_coin() # Coin already inserted.
machine.select_item() # Item selected.
machine.dispense() # Dispensing item. Enjoy!
machine.dispense() # Please insert a coin first.Java:
// State interface
public interface VendingMachineState {
void insertCoin(VendingMachine context);
void selectItem(VendingMachine context);
void dispense(VendingMachine context);
}
// Context
public class VendingMachine {
private VendingMachineState currentState;
public VendingMachine() {
this.currentState = new IdleState();
}
public void setState(VendingMachineState state) {
this.currentState = state;
}
public void insertCoin() { currentState.insertCoin(this); }
public void selectItem() { currentState.selectItem(this); }
public void dispense() { currentState.dispense(this); }
}
// ConcreteState: Idle
public class IdleState implements VendingMachineState {
@Override
public void insertCoin(VendingMachine context) {
System.out.println("Coin inserted.");
context.setState(new CoinInsertedState());
}
@Override
public void selectItem(VendingMachine context) {
System.out.println("Please insert a coin first.");
}
@Override
public void dispense(VendingMachine context) {
System.out.println("Please insert a coin first.");
}
}
// ConcreteState: CoinInserted
public class CoinInsertedState implements VendingMachineState {
@Override
public void insertCoin(VendingMachine context) {
System.out.println("Coin already inserted.");
}
@Override
public void selectItem(VendingMachine context) {
System.out.println("Item selected.");
context.setState(new ItemSelectedState());
}
@Override
public void dispense(VendingMachine context) {
System.out.println("Please select an item first.");
}
}
// ConcreteState: ItemSelected
public class ItemSelectedState implements VendingMachineState {
@Override
public void insertCoin(VendingMachine context) {
System.out.println("Please wait — dispensing in progress.");
}
@Override
public void selectItem(VendingMachine context) {
System.out.println("Item already selected.");
}
@Override
public void dispense(VendingMachine context) {
System.out.println("Dispensing item. Enjoy!");
context.setState(new IdleState());
}
}The current state decides what a call does
Run the vending machine example: each call delegates to the current state, which either changes state or rejects the operation.
Each call goes to the current state object, which starts as IdleState.
Text version
- Each call goes to the current state object, which starts as IdleState.
- IdleState rejects selection because no coin has been inserted.
- IdleState accepts the coin and switches the machine to CoinInsertedState.
- CoinInsertedState rejects another coin and leaves the state unchanged.
- CoinInsertedState accepts the selection and switches the machine to ItemSelectedState.
- ItemSelectedState dispenses the item and returns the machine to IdleState.
- The next dispense call reaches IdleState, which asks for a coin.
In the State pattern, when a ConcreteState's method determines that the system should move to a new state, how is that transition typically triggered?
3. Variants & Comparisons
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| State Pattern | Each state is a class; transitions via context.setState() | OCP-compliant, each state isolated, easy to add states | More classes, object allocation per transition | Complex state machines with distinct per-state behavior |
| Enum + Switch | State stored as enum; switch in every method | Simple, all logic in one file | Violates OCP, methods grow with states | Small, stable state machines (2–3 states) |
| State Table / Map | Explicit {state: {event: (action, nextState)}} table | Very clear transition model | Less flexible for complex actions | Finite automata, protocol parsers |
Who triggers transitions:
- -driven: ConcreteState calls
context.set_state()— knows its successors - Context-driven: Context holds transition logic — states are pure behavior, no knowledge of others
State-driven is more common and more encapsulated; Context-driven is easier to visualize transitions in one place.
Choose how to represent state behavior
State classes isolate behavior, enum switches suit small stable machines, and state tables make transitions explicit.
Classes isolate states, switches keep logic in one file, and tables make transitions clear.
Text version
- Classes isolate states, switches keep logic in one file, and tables make transitions clear.
- Classes add overhead, switch methods grow with states, and tables are less flexible for complex actions.
- Use classes for complex behavior, switches for two or three stable states, and tables for protocol parsers.
A team is building a backend service that manages TCP connection states (e.g., CLOSED, LISTEN, SYN_RECEIVED, ESTABLISHED, FIN_WAIT). The states follow well-defined transition rules, and the primary concern is having a clear, readable model of which events trigger which transitions. Which approach is the best fit for this use case?
4. When to Use It (and When NOT To)
Use when:
- An object's behavior varies significantly based on its , and that behavior can be modeled as a finite set of distinct states
- You have growing
if/elif/switchblocks gated on a variable in multiple methods - State transitions are complex and you want them explicit
Don't use when:
- The state machine has only 2–3 states that won't grow — a simple enum + switch is clearer
- State-specific behavior is trivial (one line) — the overhead of extra classes isn't justified
Decision triggers:
- "Every method in this class starts with
if self.state == X: ..." → State pattern - "Adding a new state requires editing 5 methods" → State pattern
- "I have a document that can be Draft, Under Review, Approved, or Archived" → State pattern
Anti-patterns:
- State explosion: Too many states → too many classes. Consider whether some states can be combined or whether a state table is clearer.
- Putting transition logic in Context: Context becomes a god object managing all transitions. Let states manage their own successors where possible.
A team is building an e-commerce order management system. The Order class has methods like process(), cancel(), and ship(), and every one of these methods begins with a block like if self.status == 'pending': ... elif self.status == 'paid': ... elif self.status == 'shipped': .... Adding a new order status (e.g., 'backordered') requires updating all five methods. Which design decision is most appropriate here?
5. Real-World Usage
1. TCP connection states: A TCP connection moves through CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED. Each handles incoming packets differently — receive(SYN) in LISTEN starts a handshake (CLOSED replies with RST); in ESTABLISHED state it's an error. State pattern maps directly to this model.
2. Order management systems: An Order has states: PENDING → CONFIRMED → SHIPPED → DELIVERED, plus CANCELLED. Each state defines which operations are valid (ship() is only valid in CONFIRMED; cancel() is invalid after SHIPPED). State pattern prevents invalid transitions without giant if blocks.
3. Java thread states: A Java Thread has states NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED defined by Thread.State enum. The JVM's internal thread scheduler behaves like a State machine — some operations depend on the state, for example start() is legal only in NEW.
An e-commerce platform models orders with the states: PENDING → CONFIRMED → SHIPPED → DELIVERED. A developer needs to implement a cancel() operation. Using the State pattern, what is the most appropriate way to handle calling cancel() on an order that is already in the SHIPPED state?
6. Interview Cheat Sheet
Key sentences:
- " replaces -conditional logic scattered across methods with — each state is a class that implements the full behavior for that state."
- "The key benefit is Open/Closed: adding a new state means adding a new class, not editing every existing method."
- "There are two transition models: state-driven (ConcreteState calls
context.setState()) and context-driven (Context holds the transition table). State-driven is more encapsulated." - "State and look identical structurally — both delegate behavior to an encapsulated object. The difference is intent: State transitions internally based on object lifecycle; is injected externally by the client."
Common follow-up questions:
Q: How is State different from Strategy? A: Structurally nearly identical. The difference is semantic and in who drives changes: State transitions happen based on the object's own logic (lifecycle-driven); Strategy is chosen and injected by the client. A Strategy doesn't know about other Strategies; a State typically knows its successor states.
Q: Who should own the transition logic — the state or the context? A: Either can work. State-driven is more encapsulated and keeps transition logic close to the state that triggers it. Context-driven makes all transitions visible in one place. I'd choose state-driven for complex systems and context-driven when a visual state table is needed for clarity.
Q: How do you test state machines?
A: Unit-test each ConcreteState independently by creating it directly and calling its methods with a mock Context. Test transitions by asserting context.setState() was called with the expected next state.
Connections:
- State vs. Strategy → same structure, different intent and transition ownership
- State + → Commands can trigger state transitions
- State is the object-oriented formalization of a Finite State Machine (FSM)