Chapter 05 · Article 26 of 55
State Pattern
Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Article outline15 sections on this page+
Overview
Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Also Known As: Objects for States, State Objects
Category: Behavioural Design Pattern
The State pattern encapsulates state-specific behavior into separate classes and delegates behavior to the current state object. Instead of using massive if/else or switch blocks to determine what action to take based on the current state, the object delegates the work to a state object that represents the current state. When the state changes, the object swaps its state object, effectively changing its behavior at runtime - giving the illusion that the object has changed its class.
One-liner: Replace conditionals with polymorphism - each state becomes a class that knows how to handle requests for that state.
Analogy: Think of a traffic light. The physical box is the same (Context), but its behavior - which cars can go, which must stop - depends entirely on which light is currently active (State). The light doesn't use if (color == RED) internally; it simply is red, and red knows what red means.
Pattern Classification: This is a behavioral pattern because it's concerned with the assignment of responsibilities between objects and the communication patterns between them. It specifically addresses how an object's behavior changes over its lifetime.
Problem It Solves
Consider a Document class that behaves differently depending on whether it is in Draft, Moderation, or Published state. A naive implementation leads to:
-
Complex conditional logic based on state - Every method contains
if (state == DRAFT) ... else if (state == MODERATION) ... else if (state == PUBLISHED) .... As states grow, these conditionals become unmanageable. -
Scattered state transitions - Transition rules (e.g., "Draft can move to Moderation but not directly to Published") are spread across multiple methods, making it easy to introduce invalid transitions.
-
Violating the Open/Closed Principle (OCP) - Adding a new state (e.g.,
Archived) requires modifying every method that contains state-conditional logic. You cannot extend behavior without modifying existing code. -
Duplicated guard logic - The same state-checking boilerplate is repeated in every operation, increasing the surface area for bugs.
-
Difficult testing - You cannot test one state's behavior in isolation; you must set up the entire object and manipulate its internal enum/flag.
The State pattern solves all of these by extracting each state into its own class with a well-defined interface.
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| Object behavior changes significantly based on its state | Object has only 2 simple states (a boolean suffices) |
You have large switch/if-else blocks checking state | State transitions are trivial and unlikely to grow |
| New states are expected to be added over time | The overhead of extra classes is unjustified |
| State transitions have complex rules and guards | Behavior differences between states are minimal |
| You need to test state-specific behavior in isolation | States don't have distinct behavior - only data differs |
| Multiple methods all behave differently per state | You need a simple flag, not polymorphic behavior |
| You want to make state transitions explicit and auditable | Performance-critical hot paths where vtable dispatch matters |
Key Concepts & Theory
Core Participants
-
Context - The object whose behavior varies. It maintains a reference to the current
Stateobject and delegates state-specific work to it. Clients interact with the Context. -
State (Interface/Abstract Class) - Declares the interface for state-specific behavior. All concrete states implement this interface, ensuring the Context can work with any state uniformly.
-
ConcreteState (A, B, C...) - Each class encapsulates the behavior associated with a particular state. It implements the State interface and may trigger transitions to other states.
State Transitions
A transition is the act of replacing the current state object with another. Two ownership models exist:
- State-driven transitions - Each ConcreteState decides the next state and calls
context.setState(new NextState()). States are aware of each other. - Context-driven transitions - The Context decides transitions based on return values or events from the current state. States remain decoupled.
Finite State Machines (FSM)
The State pattern is an object-oriented implementation of a Finite State Machine. An FSM is defined by:
- A finite set of states
- A finite set of events (inputs/triggers)
- A transition function: (currentState, event) → nextState
- An initial state
- Optionally, a set of final/accepting states
The State pattern maps each FSM state to a ConcreteState class and each event to a method on the State interface.
State vs Status
| Aspect | State (Pattern) | Status (Field) |
|---|---|---|
| Representation | Object with behavior | Enum/string/int flag |
| Behavior | Encapsulated in state class | Conditionals in host class |
| Transitions | Enforced by state objects | Ad-hoc, scattered |
| Extensibility | Add new class (OCP) | Modify existing conditionals |
| Testability | Test each state in isolation | Test entire object |
ASCII Class Diagram
┌─────────────────────────────┐
│ Context │
├─────────────────────────────┤
│ - state: State │
├─────────────────────────────┤
│ + request() │
│ + setState(s: State) │
└──────────────┬──────────────┘
│ delegates to
▼
┌─────────────────────────────┐
│ <<interface>> │
│ State │
├─────────────────────────────┤
│ + handleRequest(ctx) │
│ + handleAnotherOp(ctx) │
└──────────────┬──────────────┘
│
┌───────┼────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ConcreteA │ │ConcreteB │ │ConcreteC │
├──────────┤ ├──────────┤ ├──────────┤
│+handleReq│ │+handleReq│ │+handleReq│
│ → may │ │ → may │ │ → may │
│ transition│ │ transition│ │ transition│
│ to B │ │ to C │ │ to A │
└──────────┘ └──────────┘ └──────────┘
Flow: Context.request() → state.handleRequest(this) → ConcreteState executes logic and optionally calls ctx.setState(new NextState()).
Pseudocode Implementation
Example 1: Vending Machine
// State Interface
interface VendingMachineState {
insertCoin(machine: VendingMachine)
selectProduct(machine: VendingMachine)
dispense(machine: VendingMachine)
}
// Context
class VendingMachine {
private state: VendingMachineState
private stock: int
constructor(stock) {
this.stock = stock
this.state = stock > 0 ? new IdleState() : new OutOfStockState()
}
setState(s: VendingMachineState) { this.state = s }
getStock(): int { return this.stock }
decrementStock() { this.stock-- }
insertCoin() { this.state.insertCoin(this) }
selectProduct() { this.state.selectProduct(this) }
dispense() { this.state.dispense(this) }
}
// Concrete States
class IdleState implements VendingMachineState {
insertCoin(machine) {
print("Coin accepted")
machine.setState(new HasMoneyState())
}
selectProduct(machine) { print("Insert coin first") }
dispense(machine) { print("Insert coin first") }
}
class HasMoneyState implements VendingMachineState {
insertCoin(machine) { print("Coin already inserted") }
selectProduct(machine) {
print("Product selected")
machine.setState(new DispensingState())
}
dispense(machine) { print("Select a product first") }
}
class DispensingState implements VendingMachineState {
insertCoin(machine) { print("Please wait, dispensing") }
selectProduct(machine) { print("Please wait, dispensing") }
dispense(machine) {
print("Dispensing product...")
machine.decrementStock()
if (machine.getStock() > 0)
machine.setState(new IdleState())
else
machine.setState(new OutOfStockState())
}
}
class OutOfStockState implements VendingMachineState {
insertCoin(machine) { print("Machine is out of stock") }
selectProduct(machine) { print("Machine is out of stock") }
dispense(machine) { print("Machine is out of stock") }
}
Example 2: Document Workflow
interface DocumentState {
edit(doc: Document)
submit(doc: Document)
approve(doc: Document)
reject(doc: Document)
}
class Document {
private state: DocumentState = new DraftState()
private content: string
setState(s: DocumentState) { this.state = s }
edit() { this.state.edit(this) }
submit() { this.state.submit(this) }
approve() { this.state.approve(this) }
reject() { this.state.reject(this) }
}
class DraftState implements DocumentState {
edit(doc) { print("Editing document...") }
submit(doc) {
print("Submitting for moderation")
doc.setState(new ModerationState())
}
approve(doc) { print("Cannot approve a draft directly") }
reject(doc) { print("Cannot reject a draft") }
}
class ModerationState implements DocumentState {
edit(doc) { print("Cannot edit during moderation") }
submit(doc) { print("Already in moderation") }
approve(doc) {
print("Document approved and published")
doc.setState(new PublishedState())
}
reject(doc) {
print("Document rejected, back to draft")
doc.setState(new DraftState())
}
}
class PublishedState implements DocumentState {
edit(doc) { print("Cannot edit published document") }
submit(doc) { print("Already published") }
approve(doc) { print("Already published") }
reject(doc) {
print("Unpublishing, back to draft")
doc.setState(new DraftState())
}
}
State Transition Diagram
Vending Machine FSM
insertCoin()
┌──────────────────────────────┐
│ ▼
┌─────────┐ selectProduct() ┌────────────┐
│ IDLE │ ◄── reject ────── │ HAS_MONEY │
└─────────┘ └─────┬──────┘
▲ │ selectProduct()
│ stock > 0 ▼
│ ┌─────────────┐
└──────────────────────── │ DISPENSING │
└──────┬──────┘
│ stock == 0
▼
┌──────────────┐
│ OUT_OF_STOCK │
└──────────────┘
Document Workflow FSM
┌─────────┐ submit() ┌────────────┐ approve() ┌───────────┐
│ DRAFT │ ────────────► │ MODERATION │ ────────────► │ PUBLISHED │
└─────────┘ └────────────┘ └───────────┘
▲ │ │
│ reject() │ reject() │
└──────────────────────────┘ │
└───────────────────────────────────────────────────────┘
State vs Strategy
This is the most commonly confused pair in design pattern interviews. Both use composition and polymorphism, but their intent differs fundamentally.
| Dimension | State Pattern | Strategy Pattern |
|---|---|---|
| Intent | Model state-dependent behavior; object changes behavior as state changes | Define a family of algorithms; let client choose one |
| Who triggers change | State objects themselves (or Context based on events) | Client explicitly sets the strategy |
| Awareness of siblings | ConcreteStates often know about other states (for transitions) | Strategies are unaware of each other |
| Number of changes | State changes many times during object lifetime | Strategy is typically set once (or rarely changed) |
| Transition logic | Built into the pattern - transitions are first-class | No concept of transitions |
| Replaces | Complex state-based conditionals | Algorithm-selection conditionals |
| Object identity | Object "appears to change its class" | Object behavior is configured |
| Example | TCP connection (Listen → Established → Closed) | Sorting (QuickSort, MergeSort, HeapSort) |
| State coupling | High - states reference each other | Low - strategies are independent |
| Lifecycle | States are created/destroyed as transitions occur | Strategy lives as long as it's needed |
Interview tip: If the interviewer asks "How is State different from Strategy?", emphasize: State is about transitioning between behaviors over time; Strategy is about selecting a behavior at configuration time.
Real-World Examples
1. TCP Connection States
States: LISTEN, SYN_SENT, SYN_RECEIVED, ESTABLISHED, FIN_WAIT_1, FIN_WAIT_2, CLOSE_WAIT, CLOSING, LAST_ACK, TIME_WAIT, CLOSED. Each state handles open(), close(), acknowledge() differently. The GoF book uses this as its canonical example. For instance, calling close() on an ESTABLISHED connection initiates the four-way handshake, while calling close() on a LISTEN state simply deallocates resources.
2. Order Lifecycle (E-commerce)
States: Created → Confirmed → Shipped → Delivered → Returned/Cancelled. Each state determines which operations are valid (e.g., you cannot ship a cancelled order). State transitions trigger side effects (send email, update inventory, charge payment). The cancel() operation behaves differently in each state - in Created it simply removes the order, in Shipped it initiates a return shipment, and in Delivered it starts a refund process.
3. Media Player
States: Playing, Paused, Stopped. The play() button behaves differently in each state - in Stopped it starts from the beginning, in Paused it resumes from the last position, in Playing it does nothing or restarts. next()/previous() may only work in Playing state. Volume and seek operations may be available in all states except Stopped.
4. Traffic Light Controller
States: Red, Yellow, Green. Each state knows its duration and the next state. The tick() method either decrements a timer or triggers a transition. Emergency override can force any state to Red. Pedestrian crossing requests queue a transition to Red after the current green phase completes. This is a classic example of a timed state machine.
5. Game Character States
States: Idle, Running, Jumping, Attacking, Dead. Input handling differs per state - pressing "jump" while Jumping does nothing (no double-jump), pressing "attack" while Dead is ignored. Animation, physics, and collision behavior all vary by state. Transitions have prerequisites: you can only enter Attacking from Idle or Running, not from Jumping. This naturally prevents invalid animation blending.
6. Workflow/Approval Engines
States: Pending, UnderReview, Approved, Rejected, Escalated. Each state defines who can act, what actions are available, and SLA timers. Enterprise BPM systems are essentially large state machines. Jira ticket workflows, GitHub PR states (open/draft/review/merged/closed), and CI/CD pipeline stages all follow this pattern.
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Eliminates complex conditional logic | Increases number of classes (one per state) |
| Each state is cohesive and testable in isolation | Can be overkill for objects with few states |
| Adding new states doesn't modify existing code (OCP) | State classes may have tight coupling to each other |
| Makes state transitions explicit and auditable | Transition logic can be hard to trace across classes |
| Localizes state-specific behavior | Context interface may bloat if states need different methods |
| Enables runtime state inspection and logging | Memory overhead if states carry data (vs. flyweight/singleton states) |
| Natural mapping to FSM diagrams and specifications | Debugging requires understanding which state is active |
Constraints & Edge Cases
Who Owns Transitions?
| Approach | Pros | Cons |
|---|---|---|
State-driven (state calls ctx.setState(...)) | Transitions co-located with behavior; self-contained states | States coupled to each other; harder to reuse states across contexts |
| Context-driven (context decides based on result) | States are decoupled; reusable | Transition logic centralized - can become a switch statement again |
| Transition table (external config/map) | Declarative; easy to visualize and validate | Loses the behavioral encapsulation benefit |
Recommendation: Use state-driven transitions for most cases. Use a transition table when you need runtime-configurable workflows.
State Explosion
When the number of states grows combinatorially (e.g., a character that can be Running + Attacking + Shielded), a flat state machine explodes. Solutions:
- Hierarchical State Machines (HSMs) - States contain sub-states (Harel Statecharts).
- Parallel/Orthogonal regions - Independent state machines running concurrently.
- Component-based approach - Separate state machines for movement, combat, etc.
Shared State Instances (Flyweight States)
If ConcreteState classes are stateless (no instance fields), they can be shared as singletons or flyweight objects. This eliminates repeated allocation:
// Singleton states - no per-instance data
IdleState.INSTANCE
HasMoneyState.INSTANCE
Use this when states carry no context-specific data. Avoid when states store per-context information.
History / Undo of State Changes
To support undo or "go back to previous state":
- Maintain a state stack in the Context.
- Push the current state before transitioning.
undo()pops the stack and restores the previous state.
This is useful in wizards, multi-step forms, and game state management.
Thread Safety
If the Context is accessed from multiple threads, state transitions must be atomic. Options:
- Synchronize
setState()and all delegating methods. - Use an event queue (actor model) to serialize state changes.
- Use compare-and-swap for lock-free transitions.
- Leverage language-specific concurrency primitives (e.g.,
synchronizedin Java,Mutexin Rust).
Race conditions in state transitions can lead to impossible states - for example, two threads both reading the current state as Idle and both transitioning to HasMoney, resulting in double-charging. Always treat state transitions as critical sections.
Entry and Exit Actions
Many real-world state machines need to perform actions when entering or leaving a state (e.g., start a timer on entry, cancel it on exit). Implement onEnter() and onExit() hooks in the State interface:
interface State {
onEnter(ctx: Context) // called when transitioning INTO this state
onExit(ctx: Context) // called when transitioning OUT OF this state
handle(ctx: Context)
}
This is especially important in UI state machines (show/hide loading spinners) and game states (play/stop animations).
Interview Follow-ups
Q1: How does the State pattern differ from using an enum with a switch statement?
Answer: An enum + switch centralizes all behavior in one class, violating SRP and OCP. Adding a new state requires modifying every switch block. The State pattern distributes behavior across classes - each state is self-contained, independently testable, and adding a new state means adding a new class without touching existing ones. The trade-off is more classes vs. simpler code navigation.
Q2: Can a state object hold data, or should it be stateless?
Answer: Both are valid. Stateless states can be shared (flyweight/singleton), reducing memory allocation. Stateful states carry context-specific data (e.g., a timer counting down in DispensingState). If a state needs data, it should receive it via the Context or constructor. The choice depends on whether the state's behavior varies per instance.
Q3: How would you persist state in a database for a long-running workflow?
Answer: Store the state name (or enum value) in the database. On load, use a factory or registry to reconstruct the appropriate state object: StateFactory.fromName("MODERATION") → new ModerationState(). This separates the serializable identifier from the behavioral object. For complex workflows, consider event sourcing - store the sequence of transitions and replay them to reconstruct current state.
Q4 (Hint only): How would you implement a state machine that supports parallel/concurrent states?
Hint: Think orthogonal regions (Harel Statecharts). The Context holds multiple independent state references, each handling a different concern. Events are dispatched to all active regions.
Q5 (Hint only): How would you add guard conditions to transitions (e.g., "can only transition to Shipped if payment is confirmed")?
Hint: Introduce a canTransition() or guard check within the state's method before calling setState(). Alternatively, use a transition table where each entry has a predicate/guard function that must return true.
Counter Questions to Ask Interviewer
Use these to clarify scope and demonstrate systems thinking:
-
"How many states are we expecting? Is the set fixed or will it grow?" - Determines if the pattern is justified or overkill.
-
"Are state transitions triggered by external events, timers, or both?" - Influences whether you need an event-driven architecture or simple method calls.
-
"Do we need to persist state across restarts or is this in-memory only?" - Drives serialization strategy and whether you need a state registry.
-
"Can multiple transitions happen concurrently, or is this single-threaded?" - Determines thread-safety requirements.
-
"Should we support undo/rollback of state transitions?" - Influences whether you need a state history stack.
-
"Are there any states that share significant behavior?" - May suggest using an abstract base state class or hierarchical states.
References & Whitepapers
-
Gamma, Helm, Johnson, Vlissides - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 5: State Pattern, pp. 305-313.
-
David Harel - "Statecharts: A Visual Formalism for Complex Systems", Science of Computer Programming, Vol. 8, 1987. Introduced hierarchical and concurrent state machines that extend flat FSMs.
-
Martin Fowler - "State Machine" in Domain-Specific Languages (2010). Discusses implementing state machines with internal DSLs.
-
UML Specification - OMG Unified Modeling Language, State Machine Diagrams (Section 14). Formalizes states, transitions, guards, actions, and composite states.
-
Robert C. Martin - Agile Software Development (2002), Chapter on State Pattern and FSM implementation in embedded systems.
-
Miro Samek - Practical UML Statecharts in C/C++ (2008). Deep dive into hierarchical state machines for real-time embedded systems.
Related Topics
- Strategy Pattern - Most confused with State; compare intent and transition semantics
- Command Pattern - Can be combined with State for undoable state transitions
- Observer Pattern - Notify listeners when state transitions occur
- Singleton Pattern - Stateless state objects can be singletons
- Flyweight Pattern - Share stateless state instances
- Template Method - Alternative for varying steps; State varies entire behavior
- Mediator Pattern - Can coordinate transitions in complex multi-object state systems
- Memento Pattern - Store and restore previous states for undo functionality