State Pattern: Manage Object State Transitions

Updated 2026-03-11

TL;DR

The State Pattern allows an object to change its behavior when its internal state changes, making the object appear to change its class. Instead of using complex conditional logic (if/else or switch statements) to handle different behaviors based on state, each state becomes its own class with specific behavior.

Prerequisites: Understanding of classes and objects, inheritance and polymorphism, basic interface/abstract class concepts, and familiarity with the Strategy Pattern (helpful but not required).

After this topic: Implement the State Pattern to eliminate complex conditional logic in stateful objects, design clean state transitions, and recognize when state-dependent behavior should be encapsulated in separate state classes rather than handled with conditionals.

Core Concept

What is the State Pattern?

The State Pattern is a behavioral design pattern that encapsulates state-specific behavior into separate classes and delegates behavior to the current state object. The context object maintains a reference to a state object that represents its current state, and when the state changes, it switches to a different state object.

Why Use the State Pattern?

Without the State Pattern, objects with state-dependent behavior often contain large conditional statements (if/else or switch) that check the current state before executing any action. As states and transitions grow, this code becomes:

  • Hard to maintain: Adding new states requires modifying multiple methods
  • Error-prone: Easy to miss a state check somewhere
  • Violates Open/Closed Principle: Must modify existing code to add states

The State Pattern solves this by making states first-class objects. Each state knows how to handle requests and when to transition to other states.

Key Components

  1. Context: The object whose behavior changes based on state. Maintains a reference to the current state object.
  2. State Interface: Defines methods that all concrete states must implement.
  3. Concrete States: Each implements the state interface with behavior specific to that state. May trigger state transitions.

When to Use

  • An object’s behavior depends on its state and must change at runtime
  • Operations have large, multi-part conditional statements based on state
  • State transitions follow clear rules
  • You want to avoid duplicate code across similar states

State vs. Strategy Pattern

Both patterns use composition and delegation, but:

  • Strategy: Client chooses which algorithm to use; strategies don’t know about each other
  • State: States often trigger transitions to other states; the pattern manages state changes internally

Visual Guide

State Pattern Structure

classDiagram
    class Context {
        -State currentState
        +request()
        +setState(State)
    }
    class State {
        <<interface>>
        +handle(Context)
    }
    class ConcreteStateA {
        +handle(Context)
    }
    class ConcreteStateB {
        +handle(Context)
    }
    class ConcreteStateC {
        +handle(Context)
    }
    
    Context --> State
    State <|.. ConcreteStateA
    State <|.. ConcreteStateB
    State <|.. ConcreteStateC
    ConcreteStateA ..> ConcreteStateB : transitions to
    ConcreteStateB ..> ConcreteStateC : transitions to

The Context delegates behavior to its current State object. States can trigger transitions by changing the Context’s state.

Vending Machine State Transitions

stateDiagram-v2
    [*] --> NoCoin
    NoCoin --> HasCoin: insert coin
    HasCoin --> NoCoin: eject coin
    HasCoin --> Sold: select product
    Sold --> NoCoin: dispense
    Sold --> SoldOut: dispense (last item)
    SoldOut --> [*]

Example state machine for a vending machine showing valid state transitions.

Examples

Example 1: Document Workflow System

A document goes through different states: Draft, Moderation, Published. Each state has different behavior for publish() and render().

from abc import ABC, abstractmethod

# State Interface
class DocumentState(ABC):
    @abstractmethod
    def publish(self, document):
        pass
    
    @abstractmethod
    def render(self):
        pass

# Concrete States
class DraftState(DocumentState):
    def publish(self, document):
        print("Draft: Sending to moderation...")
        document.set_state(ModerationState())
    
    def render(self):
        return "[DRAFT] Content visible only to author"

class ModerationState(DocumentState):
    def publish(self, document):
        print("Moderation: Approving and publishing...")
        document.set_state(PublishedState())
    
    def render(self):
        return "[MODERATION] Content visible to moderators"

class PublishedState(DocumentState):
    def publish(self, document):
        print("Published: Already published!")
    
    def render(self):
        return "[PUBLISHED] Content visible to everyone"

# Context
class Document:
    def __init__(self, content):
        self.content = content
        self._state = DraftState()  # Initial state
    
    def set_state(self, state):
        self._state = state
    
    def publish(self):
        self._state.publish(self)
    
    def render(self):
        return f"{self._state.render()}\n{self.content}"

# Usage
doc = Document("Important article content")
print(doc.render())
# Output:
# [DRAFT] Content visible only to author
# Important article content

doc.publish()
# Output: Draft: Sending to moderation...

print(doc.render())
# Output:
# [MODERATION] Content visible to moderators
# Important article content

doc.publish()
# Output: Moderation: Approving and publishing...

print(doc.render())
# Output:
# [PUBLISHED] Content visible to everyone
# Important article content

doc.publish()
# Output: Published: Already published!

Key Points:

  • Each state handles publish() differently
  • States trigger transitions by calling document.set_state()
  • No conditionals in the Document class
  • Adding a new state (e.g., “Rejected”) requires creating one new class

Try it yourself: Add a RejectedState that can transition back to DraftState when the author makes edits.

Example 2: TCP Connection

A TCP connection has states: Closed, Listen, Established. Each state responds differently to open(), close(), and acknowledge().

from abc import ABC, abstractmethod

class TCPState(ABC):
    @abstractmethod
    def open(self, connection):
        pass
    
    @abstractmethod
    def close(self, connection):
        pass
    
    @abstractmethod
    def acknowledge(self, connection):
        pass

class ClosedState(TCPState):
    def open(self, connection):
        print("Opening connection...")
        connection.set_state(ListenState())
    
    def close(self, connection):
        print("Already closed")
    
    def acknowledge(self, connection):
        print("Error: Cannot acknowledge, connection closed")

class ListenState(TCPState):
    def open(self, connection):
        print("Already listening")
    
    def close(self, connection):
        print("Closing connection...")
        connection.set_state(ClosedState())
    
    def acknowledge(self, connection):
        print("Received ACK, establishing connection...")
        connection.set_state(EstablishedState())

class EstablishedState(TCPState):
    def open(self, connection):
        print("Already established")
    
    def close(self, connection):
        print("Closing established connection...")
        connection.set_state(ClosedState())
    
    def acknowledge(self, connection):
        print("Data acknowledged")

class TCPConnection:
    def __init__(self):
        self._state = ClosedState()
    
    def set_state(self, state):
        self._state = state
    
    def open(self):
        self._state.open(self)
    
    def close(self):
        self._state.close(self)
    
    def acknowledge(self):
        self._state.acknowledge(self)
    
    def get_state_name(self):
        return self._state.__class__.__name__

# Usage
conn = TCPConnection()
print(f"State: {conn.get_state_name()}")  # Output: State: ClosedState

conn.acknowledge()
# Output: Error: Cannot acknowledge, connection closed

conn.open()
# Output: Opening connection...
print(f"State: {conn.get_state_name()}")  # Output: State: ListenState

conn.acknowledge()
# Output: Received ACK, establishing connection...
print(f"State: {conn.get_state_name()}")  # Output: State: EstablishedState

conn.acknowledge()
# Output: Data acknowledged

conn.close()
# Output: Closing established connection...
print(f"State: {conn.get_state_name()}")  # Output: State: ClosedState

Key Points:

  • Invalid operations are handled gracefully by each state
  • State transitions follow TCP protocol rules
  • Easy to test each state in isolation

Java/C++ Note: In Java, you’d typically use interfaces for the State and have the Context hold a State reference. In C++, use abstract base classes with pure virtual functions and smart pointers (std::unique_ptr<State>).

Try it yourself: Add a TimeWaitState that occurs before transitioning from Established to Closed, with a timeout mechanism.

Common Mistakes

1. Making States Stateful

Mistake: Storing data in state objects that should belong to the context.

# WRONG: State holds document-specific data
class DraftState:
    def __init__(self, content):
        self.content = content  # Bad: content belongs to Document

Why it’s wrong: State objects should typically be stateless and shareable. Context-specific data belongs in the Context class. States should only contain behavior.

Correct approach: Keep data in the Context, pass it to state methods as needed.

2. Not Sharing State Instances

Mistake: Creating new state instances for every transition.

# INEFFICIENT: Creates new objects repeatedly
def publish(self, document):
    document.set_state(ModerationState())  # New instance every time

Why it’s wrong: If states are stateless, you can use the Flyweight Pattern and share single instances.

Better approach:

# Singleton-like state instances
class States:
    DRAFT = DraftState()
    MODERATION = ModerationState()
    PUBLISHED = PublishedState()

def publish(self, document):
    document.set_state(States.MODERATION)  # Reuse instance

3. Putting Transition Logic in Context

Mistake: The Context decides which state to transition to.

# WRONG: Context knows about state transitions
class Document:
    def publish(self):
        if isinstance(self._state, DraftState):
            self._state = ModerationState()
        elif isinstance(self._state, ModerationState):
            self._state = PublishedState()

Why it’s wrong: This defeats the purpose of the pattern. You still have conditionals based on state type.

Correct approach: States should trigger their own transitions by calling context.set_state().

4. Overusing the Pattern for Simple Cases

Mistake: Using State Pattern when a simple boolean or enum would suffice.

# OVERKILL for a simple on/off switch
class OnState: ...
class OffState: ...

Why it’s wrong: The pattern adds complexity. Use it when you have:

  • Multiple states (3+)
  • Complex state-dependent behavior
  • Frequent state changes

Simple cases: Use a boolean flag or enum with a single conditional.

5. Forgetting to Handle Invalid Transitions

Mistake: Not considering what happens when an invalid operation is called in a state.

# INCOMPLETE: What if publish() is called on PublishedState?
class PublishedState:
    def publish(self, document):
        pass  # Silent failure

Why it’s wrong: Silent failures hide bugs. Invalid operations should either raise exceptions or log warnings.

Better approach:

def publish(self, document):
    raise InvalidStateTransition("Cannot publish an already published document")
    # Or: print a warning and do nothing

Interview Tips

Recognition Questions

Interviewer asks: “How would you design a traffic light system?”

Your answer should include: Mention the State Pattern explicitly. Explain that each light (Red, Yellow, Green) is a state with different behavior for next() and getColor(). Show how states transition to each other.

Implementation Questions

Be ready to:

  1. Draw the class diagram showing Context, State interface, and concrete states
  2. Explain the difference between State and Strategy patterns (common follow-up)
  3. Discuss state storage: Should states be singletons? When should they hold data?
  4. Handle edge cases: What happens with invalid transitions?

Code Design Questions

Common scenario: “Design an order processing system with states: Pending, Paid, Shipped, Delivered, Cancelled.”

Strong answer structure:

  1. Identify the Context (Order) and states
  2. Define the State interface with methods like pay(), ship(), cancel()
  3. Show 2-3 concrete states with transition logic
  4. Explain which transitions are valid from each state
  5. Discuss error handling for invalid transitions

Comparison Questions

“When would you use State Pattern vs. a simple state machine with enums?”

Good answer:

  • Use enums + switch when: Few states (2-3), simple behavior, no complex transitions
  • Use State Pattern when: Many states, complex state-specific behavior, behavior changes frequently, need to add states without modifying existing code

Red Flags to Avoid

  • Saying “I’d use if/else statements to check the state” (misses the entire point)
  • Not knowing the difference between State and Strategy
  • Putting all transition logic in the Context class
  • Creating tightly coupled states that directly reference each other (instead of going through Context)

Bonus Points

  • Mention state history/undo: “We could maintain a state stack to implement undo functionality”
  • Discuss concurrent states: “For complex systems, we might need hierarchical state machines”
  • Reference real-world examples: TCP connections, game character states, UI component states, workflow engines

Key Takeaways

  • The State Pattern encapsulates state-specific behavior into separate classes, eliminating complex conditionals and making states first-class objects that can be easily extended.
  • States control their own transitions by calling context.set_state(), keeping transition logic close to the behavior it affects rather than centralized in the context.
  • Use the pattern when you have 3+ states with distinct behaviors, not for simple boolean flags or when state transitions are trivial.
  • Keep state objects stateless and shareable (like singletons) to avoid unnecessary object creation; context-specific data belongs in the Context class.
  • The pattern follows the Open/Closed Principle: adding new states requires creating new classes without modifying existing code, making the system more maintainable and testable.