State Pattern: Manage Object State Transitions
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.
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
- Context: The object whose behavior changes based on state. Maintains a reference to the current state object.
- State Interface: Defines methods that all concrete states must implement.
- 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:
- Draw the class diagram showing Context, State interface, and concrete states
- Explain the difference between State and Strategy patterns (common follow-up)
- Discuss state storage: Should states be singletons? When should they hold data?
- 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:
- Identify the Context (Order) and states
- Define the State interface with methods like
pay(),ship(),cancel() - Show 2-3 concrete states with transition logic
- Explain which transitions are valid from each state
- 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.