State Pattern
State models a context that behaves differently depending on which state object it holds.
Introduction
State models a context that behaves differently depending on which state object it holds. Each state encapsulates rules for that phase plus legal transitions. Order: Pending → Paid → Shipped → Cancelled — each allows different operations. Without State you get nested switches that grow into bug factories.
The story
A subscription billing system had a 700-line method updating status across nine states. Adding "Paused" caused six regressions. One State class per phase with explicit transitions turned bugs into "transition not allowed" exceptions — Paused shipped in 200 lines.
The business problem
Status-field switches scatter transition rules and allow invalid states:
- Switch sprawl: if (status === PAID) in every method.
- Invalid transitions: Paid → Pending silently corrupts data.
- New state pain: adding Paused edits every method.
- Scattered invariants: "cancel only before ship" duplicated everywhere.
The problem teams faced
State fits when:
- Behaviour depends on status/mode/phase checked in many methods.
- Adding a state requires editing every method.
- Invalid transitions are a production bug class.
- State-specific rules and transitions belong together.
Understanding the topic
Intent: Alter behaviour when internal state changes — object appears to change class.
- Context — owns current State; delegates operations.
- State — interface for state-specific behaviour.
- ConcreteState — implements one phase; calls context.setState() to transition.
Internal architecture
Order state machine:
interface OrderState {pay(order: Order): voidship(order: Order): voidcancel(order: Order): void}class PendingState implements OrderState {pay(order: Order) { order.setState(new PaidState()); /* charge */ }ship(order: Order) { throw new IllegalTransitionError("pending", "ship") }cancel(order: Order) { order.setState(new CancelledState()) }}class Order {private state: OrderState = new PendingState()setState(s: OrderState) { this.state = s }pay() { this.state.pay(this) }}
Visual explanation
Three diagrams cover state delegation, transitions, and vs Strategy:
Informative example
Before / after — status switch to State objects:
// ❌ 700-line method — adding Paused breaks six pathsfunction updateSubscription(sub: Subscription, action: string) {if (sub.status === "ACTIVE" && action === "PAUSE") { /* ... */ }else if (sub.status === "PAUSED" && action === "RESUME") { /* ... */ }// ... dozens more}// ✅ State objects — new state = new classclass ActiveState implements SubState {pause(sub: Subscription) { sub.setState(new PausedState()); sub.billing.hold() }cancel(sub: Subscription) { sub.setState(new CancelledState()) }}class PausedState implements SubState {resume(sub: Subscription) { sub.setState(new ActiveState()); sub.billing.resume() }cancel(sub: Subscription) { sub.setState(new CancelledState()) }}
Execution workflow
Map states
List phases and legal transitions.
Real-world use
TCP connection states, HTTP/2 streams, order management, document workflow (draft/review/published), JVM thread states, Kubernetes Pod lifecycle, Stripe PaymentIntent state machine.
Production case study
Telco activation flow:
- Scenario: Eight provisioning states across mobile/wireline products.
- Decision: One State class per phase; guards on transitions; domain events emitted.
- Outcome: New product type added states without modifying existing flows.
Trade-offs
- Pro: transitions localized; illegal ops explicit; new state additive.
- Pro: state diagram maps directly to code.
- Con: many small classes for simple state machines.
- Con: transition table may be clearer for 3-state machines (YAGNI).
Decision framework
- Use when 4+ states with different behaviour per state.
- Use when invalid transitions are costly bugs.
- Avoid for 2-3 states with identical behaviour — enum suffices.
- Consider state table/library (XState) for complex UI flows.
Best practices
- Draw state diagram in ADR before implementing.
- Emit domain event on every transition for audit.
- Context thin — all rules in State classes.
Anti-patterns to avoid
- State pattern for 2-state on/off — boolean sufficient.
- Context still checking status string alongside State objects.
- God-state class handling multiple unrelated lifecycles.
Common mistakes
- Forgetting transition on one code path — orphan states in DB.
- Persisting state object identity instead of state name.
- Circular transitions without terminal states.
Debugging tips
- Log every setState with from/to and correlation ID.
- Property test: random action sequences never reach illegal states.
Optimization strategies
- Flyweight State objects when stateless — reuse singleton PendingState instance.
- State table code gen from diagram for large machines.
Common misconceptions
- State ≠ Strategy. State: internal lifecycle. Strategy: external algorithm swap.
- State machine library (XState) is State pattern with tooling.
- Enum + switch OK for 3 simple states — don't over-pattern.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionState vs Strategy?+
Answer
Follow-up
2IntermediateQuestionWhen enum enough?+
Answer
Follow-up
3AdvancedQuestionWhere invalid transition handled?+
Answer
Follow-up
4BeginnerQuestionState vs if-chain?+
Answer
Follow-up
Summary
You can replace status switches with State objects, guard transitions, and emit events on change. Tell the subscription Paused regression story — if you can do that, you own State.