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:

  1. 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.

  2. 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.

  3. 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.

  4. Duplicated guard logic - The same state-checking boilerplate is repeated in every operation, increasing the surface area for bugs.

  5. 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 UseWhen NOT to Use
Object behavior changes significantly based on its stateObject has only 2 simple states (a boolean suffices)
You have large switch/if-else blocks checking stateState transitions are trivial and unlikely to grow
New states are expected to be added over timeThe overhead of extra classes is unjustified
State transitions have complex rules and guardsBehavior differences between states are minimal
You need to test state-specific behavior in isolationStates don't have distinct behavior - only data differs
Multiple methods all behave differently per stateYou need a simple flag, not polymorphic behavior
You want to make state transitions explicit and auditablePerformance-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 State object 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:

  1. State-driven transitions - Each ConcreteState decides the next state and calls context.setState(new NextState()). States are aware of each other.
  2. 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

AspectState (Pattern)Status (Field)
RepresentationObject with behaviorEnum/string/int flag
BehaviorEncapsulated in state classConditionals in host class
TransitionsEnforced by state objectsAd-hoc, scattered
ExtensibilityAdd new class (OCP)Modify existing conditionals
TestabilityTest each state in isolationTest 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.

DimensionState PatternStrategy Pattern
IntentModel state-dependent behavior; object changes behavior as state changesDefine a family of algorithms; let client choose one
Who triggers changeState objects themselves (or Context based on events)Client explicitly sets the strategy
Awareness of siblingsConcreteStates often know about other states (for transitions)Strategies are unaware of each other
Number of changesState changes many times during object lifetimeStrategy is typically set once (or rarely changed)
Transition logicBuilt into the pattern - transitions are first-classNo concept of transitions
ReplacesComplex state-based conditionalsAlgorithm-selection conditionals
Object identityObject "appears to change its class"Object behavior is configured
ExampleTCP connection (Listen → Established → Closed)Sorting (QuickSort, MergeSort, HeapSort)
State couplingHigh - states reference each otherLow - strategies are independent
LifecycleStates are created/destroyed as transitions occurStrategy 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

AdvantagesDisadvantages
Eliminates complex conditional logicIncreases number of classes (one per state)
Each state is cohesive and testable in isolationCan 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 auditableTransition logic can be hard to trace across classes
Localizes state-specific behaviorContext interface may bloat if states need different methods
Enables runtime state inspection and loggingMemory overhead if states carry data (vs. flyweight/singleton states)
Natural mapping to FSM diagrams and specificationsDebugging requires understanding which state is active

Constraints & Edge Cases

Who Owns Transitions?

ApproachProsCons
State-driven (state calls ctx.setState(...))Transitions co-located with behavior; self-contained statesStates coupled to each other; harder to reuse states across contexts
Context-driven (context decides based on result)States are decoupled; reusableTransition logic centralized - can become a switch statement again
Transition table (external config/map)Declarative; easy to visualize and validateLoses 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., synchronized in Java, Mutex in 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:

  1. "How many states are we expecting? Is the set fixed or will it grow?" - Determines if the pattern is justified or overkill.

  2. "Are state transitions triggered by external events, timers, or both?" - Influences whether you need an event-driven architecture or simple method calls.

  3. "Do we need to persist state across restarts or is this in-memory only?" - Drives serialization strategy and whether you need a state registry.

  4. "Can multiple transitions happen concurrently, or is this single-threaded?" - Determines thread-safety requirements.

  5. "Should we support undo/rollback of state transitions?" - Influences whether you need a state history stack.

  6. "Are there any states that share significant behavior?" - May suggest using an abstract base state class or hierarchical states.


References & Whitepapers

  1. Gamma, Helm, Johnson, Vlissides - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 5: State Pattern, pp. 305-313.

  2. 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.

  3. Martin Fowler - "State Machine" in Domain-Specific Languages (2010). Discusses implementing state machines with internal DSLs.

  4. UML Specification - OMG Unified Modeling Language, State Machine Diagrams (Section 14). Formalizes states, transitions, guards, actions, and composite states.

  5. Robert C. Martin - Agile Software Development (2002), Chapter on State Pattern and FSM implementation in embedded systems.

  6. Miro Samek - Practical UML Statecharts in C/C++ (2008). Deep dive into hierarchical state machines for real-time embedded systems.