Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 35

    State Pattern

    State models a context that behaves differently depending on which state object it holds.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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:

    ts
    interface OrderState {
    pay(order: Order): void
    ship(order: Order): void
    cancel(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:

    State delegation
    Context.cancel()
    No status if
    currentState.cancel()
    Delegate
    Legal?
    PaidState allows
    setState(next)
    Transition
    Context never switches on status string — State object decides.
    Transition guard
    Pending
    pay() → Paid
    Paid
    ship() → Shipped
    Shipped
    cancel() throws
    Illegal blocked
    At boundary
    Illegal transitions fail in State class — not scattered if-checks.
    State vs Strategy
    Lifecycle phase?
    State
    Swappable algorithm?
    Strategy
    Context owns transition
    State
    External injection
    Strategy
    State changes internally through lifecycle; Strategy swapped externally.

    Informative example

    Before / after — status switch to State objects:

    ts
    // ❌ 700-line method — adding Paused breaks six paths
    function 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 class
    class 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

    1State machine workflow
    1 / 4

    Map states

    List phases and legal transitions.

    Draw state diagram before coding.

    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.

    4 questions
    1IntermediateQuestionState vs Strategy?+

    Answer

    State: context behaviour changes via internal state object through lifecycle — transitions managed inside. Strategy: algorithm injected externally, swappable without lifecycle.

    Follow-up

    Order example?
    2IntermediateQuestionWhen enum enough?+

    Answer

    2-3 states, same operations allowed everywhere, no complex transition rules. State pattern when behaviour differs per state and transitions guarded.

    Follow-up

    Paused subscription?
    3AdvancedQuestionWhere invalid transition handled?+

    Answer

    In ConcreteState method — throw IllegalTransitionError. Not scattered in Context if-checks.

    Follow-up

    Persist state name?
    4BeginnerQuestionState vs if-chain?+

    Answer

    If-chain grows O(states × operations). State localizes each state's rules — new state is new class, not edit all methods.

    Follow-up

    Draw diagram first?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.