Use when an object's behavior changes based on its internal state and the logic for each state is complex — replacing large conditional chains with state objects where each state encapsulates its own behavior.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add jeffreytse/grimoire-core --skill apply-state-pattern --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Apply State Pattern?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeffreytse-apply-state-pattern)More formats (shields.io, HTML) on the badges page.
---
name: apply-state-pattern
description: Use when an object's behavior changes based on its internal state and the logic for each state is complex — replacing large conditional chains with state objects where each state encapsulates its own behavior.
source: "Gamma, Helm, Johnson, Vlissides, \"Design Patterns: Elements of Reusable Object-Oriented Software\" (1994) pp. 305–313; TCP connection state machine; finite state machines in game AI; XState (JavaScript statechart library); vending machine controllers"
tags: [design-patterns, behavioral, state, oop, developer, state-machine, conditional-elimination]
related: [apply-strategy-pattern, apply-command-pattern, apply-solid-principles]
---
# Apply State Pattern
Allow an object to alter its behavior when its internal state changes — by delegating behavior to state objects rather than using conditional logic.
## Why This Is Best Practice
**Adopted by:** XState (JavaScript statechart library — 1.5M weekly npm downloads)
implements State pattern as its core model. TCP protocol stack implements state as
objects (CLOSED, LISTEN, SYN_SENT, ESTABLISHED, etc. — each with its own behavior for
each operation). Unity's Animator Controller is a visual state machine based on the
same pattern. Every parser generator (ANTLR, PLY) produces state-based recognizers.
**Impact:** GoF documents that without the State pattern, a class with 4 states and
6 operations has 4×6=24 conditional branches scattered across all methods. The State
pattern consolidates each state's behavior into one class — 4 state classes of 6 methods
each. Adding a 5th state requires one new class, not editing 6 existing methods.
**Why best:** `if/elif` state dispatch — the alternative — scatters state logic across
every method, making it hard to add states or understand what happens in a given state.
State objects make each state's behavior cohesive, isolated, and independently testable.
Sources: Gamma et al. (1994) pp. 305–313; XState documentation; RFC 793 (TCP state machine)
## Steps
### Step 1: Define the state interface with all state-dependent methods
```python
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def pay(self, order: "Order") -> None: ...
@abstractmethod
def ship(self, order: "Order") -> None: ...
@abstractmethod
def cancel(self, order: "Order") -> None: ...
```
### Step 2: Implement concrete state classes — each handles its own transitions
```python
class PendingState(OrderState):
def pay(self, order: "Order") -> None:
print("Payment accepted")
order.state = PaidState()
def ship(self, order: "Order") -> None:
print("Cannot ship — order not paid")
def cancel(self, order: "Order") -> None:
print("Order cancelled")
order.state = CancelledState()
class PaidState(OrderState):
def pay(self, order: "Order") -> None:
print("Already paid")
def ship(self, order: "Order") -> None:
print("Order shipped")
order.state = ShippedState()
def cancel(self, order: "Order") -> None:
print("Cancelling paid order — refund issued")
order.state = CancelledState()
class ShippedState(OrderState):
def pay(self, order: "Order") -> None:
print("Already paid and shipped")
def ship(self, order: "Order") -> None:
print("Already shipped")
def cancel(self, order: "Order") -> None:
print("Cannot cancel — already shipped")
class CancelledState(OrderState):
def pay(self, order: "Order") -> None:
print("Order is cancelled")
def ship(self, order: "Order") -> None:
print("Order is cancelled")
def cancel(self, order: "Order") -> None:
print("Already cancelled")
```
### Step 3: Context holds the current state and delegates all state-dependent operations
```python
class Order:
def __init__(self):
self.state: OrderState = PendingState()
def pay(self) -> None:
self.state.pay(self)
def ship(self) -> None:
self.state.ship(self)
def cancel(self) -> None:
self.state.cancel(self)
```
### Step 4: Transition state inside state objects — not in the context
State transitions (`order.state = PaidState()`) happen inside state methods (Step 2),
not inside `Order`. This keeps the context thin and each state's transitions explicit.
### Step 5: Verify transitions by testing each state in isolation
```python
def test_pending_pay():
order = Order()
assert isinstance(order.state, PendingState)
order.pay()
assert isinstance(order.state, PaidState)
def test_shipped_cannot_cancel():
order = Order()
order.pay()
order.ship()
order.cancel()
assert isinstance(order.state, ShippedState) # state unchanged
```
## When NOT to Use
- **Simple states with few operations** — 2 states × 2 operations = 4 conditions. A simple `if/else` is cleaner than 2 state classes.
- **When transitions are purely data-driven** — if state transitions come from a config file or database, a data-driven state machine (a transition table) is more maintainable than code-based state objects.
## Common Mistakes
**Context that checks `isinstance(state, ...)` to make decisions.** This reintroduces the conditional logic the pattern eliminates. Delegate the decision to the state object.
**State objects that modify context fields directly.** If `PaidState` sets `order.paid_at = datetime.now()`, the state is coupled to the context's internals. Pass only what state objects need, or use events.
**Missing states in the state interface.** If one state handles `ship()` but another doesn't implement the interface method, calls fail unexpectedly. All states must implement all interface methods.Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!