skip to content

questions

5

What problem does the State design pattern solve, and how does it restructure code that would otherwise branch on a status field inside many methods?

level: juniorimportance: must knowfreq 58%

answer

  1. object appears to change its class
  2. one class per state, no if/else inside
  3. context delegates to currentState
  4. transition = swap the reference
  5. states usually stateless + shared

basics

~20 s

It lets one object behave differently as its internal condition changes. Each condition becomes a small class holding the behavior for that situation, and the main object forwards calls to whichever one is current — so no giant if/else.

solid answer

~50 s

State encapsulates each mode of an object in its own class implementing a common interface. The context holds a reference to the current state object and delegates every state-dependent operation to it; a transition is simply replacing that reference. The payoff: conditionals repeated across many methods (`if status == DRAFT ... else if status == PUBLISHED ...`) collapse into polymorphic dispatch, each state's rules live in one place, and adding a state means adding a class instead of editing every switch — closer to the Open/Closed Principle. Costs: more classes, extra indirection when tracing flow, and the transition graph becomes implicit unless you centralize or document it. State objects are usually stateless and shareable; the data lives in the context, which states reach through an argument or back-reference. Guards, entry/exit actions, and how illegal operations behave (throw vs. ignore) are explicit design decisions.

code

pseudocode · 18 lines
pseudocode
interface DocState { publish(doc); edit(doc, text) }

Draft implements DocState {
  publish(doc) { doc.transitionTo(InReview) }      // allowed
  edit(doc, t) { doc.body = t }                    // allowed
}

InReview implements DocState {
  publish(doc) { doc.transitionTo(Published) }
  edit(doc, t) { reject("locked during review") }
}

class Document {
  private state = Draft                            // shared, stateless instances
  publish()      { state.publish(this) }           // pure delegation, no branching
  edit(text)     { state.edit(this, text) }
  transitionTo(next) { state.onExit(this); state = next; next.onEnter(this) }
}

go deeper

for a junior

Name the intent (behavior varies with internal state), say each state becomes a class, and that the context delegates to the current one instead of using if/else.

for a middle

Add where data lives (context, states stateless and shareable), who performs transitions, and the Open/Closed benefit versus the class-count cost.

for a senior

Discuss entry/exit hooks, a base state implementing the reject-everything default, illegal-transition policy, centralizing transitionTo, and persistence via a discriminator.

for a principal

Frame it as one implementation of a finite state machine, compare with table/data-driven dispatch and workflow engines, and connect transition legality to invariants, auditability, and side-effect safety at system scale.

## The problem Many objects have a **lifecycle**: a document is `Draft`, then `InReview`, then `Published`, then `Archived`; a network connection is `Closed`, `Connecting`, `Open`; an order is `New`, `Paid`, `Shipped`, `Cancelled`. The naive implementation stores the lifecycle position in a field (an enum or string called `status`) and every operation begins by asking what that field is: ``` class Document: status # "draft" | "review" | "published" | "archived" publish(): if status == "draft": raise "needs review first" else if status == "review": status = "published"; notifySubscribers() else if status == "published": ignore else: raise "archived documents cannot be published" edit(text): if status == "draft": body = text else if status == "review": raise "locked during review" ... archive(): ... yet another chain ... ``` Two things go wrong as the lifecycle grows: 1. **The knowledge about one state is smeared across every method.** To answer "what can I do while InReview?" you must read all of `publish`, `edit`, `archive`, `delete`. 2. **Adding a state means editing every method.** Miss one chain and you get a silent wrong behavior (the fall-through `else` quietly does the wrong thing). This violates the *Open/Closed Principle* — the idea that you should extend behavior by adding code, not by editing existing, working code. ## The pattern State (one of the *Gang of Four* behavioral patterns) says: **make each state a class**. - **State interface** — declares the state-dependent operations (`publish()`, `edit()`, `archive()`). - **Concrete states** — `DraftState`, `InReviewState`, `PublishedState`, `ArchivedState`, each implementing all operations *for that one state only*. No conditionals inside; the class *is* the condition. - **Context** — the object clients actually hold (`Document`). It keeps a field `currentState` typed as the State interface and **delegates**: `publish() { currentState.publish(this) }`. - **Transition** — assigning a different object to `currentState`. Because the field's dynamic type determines behavior, the context "appears to change its class" at runtime — which is exactly how the GoF book phrases the intent. The long `if/else` chains disappear; the language's **dynamic dispatch** (virtual method lookup) does the branching for free. ## Where the data lives A state class typically holds **no** instance data — it is pure behavior. The context owns the data (document body, order total, retry counter). States get access one of two ways: - **Parameter passing:** `state.publish(context)` — the context passes itself in. This keeps state objects **stateless**, so a single instance can be shared by every context (a *Flyweight*, often a singleton or an enum constant). Cheap: no allocation per transition. - **Back-reference:** each state instance stores `owner = context`. Simpler call sites, but now states are per-context objects and must be allocated on each transition, and you have a reference cycle to reason about. If a state genuinely needs its own data (e.g. `Connecting` holds a retry count and a deadline), per-context instances are the right call — the data is naturally scoped to that state and vanishes when you leave it. ## Who performs the transition? Two legitimate placements, both common: - **Inside the state classes** (`context.setState(new PublishedState())` at the end of `InReviewState.publish`) — each state knows its own successors. Very cohesive, but each state now depends on its neighbors and the whole transition graph exists only as scattered assignments. - **In the context** — states return a token/next-state and the context applies it, or the context consults an explicit transition table. The graph is visible in one place, easier to validate or draw; states stay ignorant of each other. Either way, the setter that swaps state should be package-private/internal, not public API, so outsiders cannot teleport an object into an arbitrary state. ## Entry and exit actions Real state machines often need work *on crossing a boundary*: start a timer when entering `Connecting`, cancel it when leaving. Model that as `onEnter(context)` / `onExit(context)` hooks on the State interface, invoked by a single `transitionTo()` method on the context. Centralizing the swap in one method is what makes such hooks reliable — if states assign `currentState` directly you will eventually forget an `onExit`. ## Illegal operations Calling `ship()` on a `Cancelled` order must do *something* well-defined. Options, chosen deliberately and applied consistently: - **Throw** an `IllegalStateException`-style error — good for programmer errors and for APIs that must be strict. - **Ignore** (no-op) — good for idempotent/event-driven systems where a duplicate event should be harmless. - **Return a result object** (`Rejected(reason)`) — good when the caller is a UI or a workflow that must show why. A **default/abstract base state** implementing every operation as "reject" and letting each concrete state override only what it allows is the standard way to avoid repeating this in every class. ## Costs and when *not* to use it - **Class count explodes.** Four states × one interface = five to six types where you had one enum. For two states and one branch, a boolean and an `if` is better engineering. - **Flow is harder to follow in a debugger** — you step into an interface call and must know the current dynamic type. - **The graph is implicit.** With many states and many events, a data-driven table or an explicit state-machine library becomes clearer than a class per state. - **Persistence needs a mapping.** You cannot store a class instance in a database column; you store a discriminator (`"IN_REVIEW"`) and rebuild the state object on load. ## Relationship to other patterns - **Strategy** has the same class diagram but a different intent: the client picks a strategy, strategies do not know each other, and the object's *identity/lifecycle* is not what changes. - **Flyweight** is often combined with State to share stateless state instances. - **Singleton/enum constants** are a common implementation vehicle for those shared instances. - **Finite State Machine (FSM)** is the underlying formal model; State is one implementation technique for an FSM, table-driven dispatch is another.

  • Do you need a new state object on every transition?
    Usually not. If state classes hold no data and receive the context as a parameter, one shared instance per state (singleton, enum constant, or a static table) serves every context — a Flyweight. Allocate per context only when a state needs its own data, such as a retry counter or deadline that should die when you leave the state.
  • How do you persist an object that uses the State pattern?
    Store a stable discriminator string/enum for the current state, never the object. On load, map the discriminator back to the state instance via a lookup table. Keep the stored names decoupled from class names so renaming a class does not break old rows, and plan a migration path for states you remove.
  • What should happen when a client calls an operation the current state forbids?
    Pick one policy and apply it everywhere: throw (strict APIs, programmer errors), no-op (idempotent, event-driven systems replaying duplicates), or return a rejection result with a reason (UI/workflow). Implement it once in an abstract base state and override only the allowed operations in each concrete state.

A vending machine. Insert a coin while it is 'idle' and it starts a purchase; insert a coin while it is 'dispensing' and it drops straight into the coin return. Same slot, same physical action — the machine's internal mode decides what happens. State pattern replaces the machine's mental checklist with a separate small machine-brain per mode, plugged in one at a time.

saying these in an interview costs you the question

  • Saying State is 'just an enum with methods' — an enum with a switch inside each method still concentrates all branching; the point is one type per state with no conditionals.
  • Making the state-swapping setter public, so any caller can force an object into an arbitrary state and skip guards and entry actions.
  • Putting the context's data inside the state objects, so it is lost on transition or duplicated.
  • Claiming State always removes conditionals — the conditional often just moves to the factory/lookup that maps a persisted discriminator back to a state object; that one is fine and unavoidable.
  • Reaching for State when there are only two modes and one branching method — the class explosion costs more than it saves.

context

open as a page

The State pattern and the Strategy pattern have nearly identical class diagrams. What actually distinguishes them, and how would you tell which one a given piece of code is using?

level: middleimportance: must knowfreq 66%

basics

~20 s

Same shape, different purpose. With Strategy the caller chooses one interchangeable algorithm and it normally stays put. With State the object swaps its own behavior as its lifecycle advances, and the states know which state comes next.

open as a page

In a State-pattern design, transitions can be decided inside the concrete state classes or centrally by the context. What are the trade-offs of each placement, and how do you keep the overall transition graph reviewable?

level: middleimportance: should knowfreq 42%

basics

~20 s

If states set the next state, each state is self-contained but the whole map is scattered across classes. If the context decides, the map lives in one readable place but the context grows. Either way, funnel every change through one transition method.

open as a page

When would you replace polymorphic State-pattern classes with a data-driven finite state machine (a transition table or a state-machine library), and what do you gain and lose by doing so?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Switch to a table when there are many states and events and the rules matter more than the code: a table is one page you can read, diagram, and validate. You lose the ability to write rich, typed, per-state behavior easily.

open as a page

How do you apply the State pattern to a long-lived business entity whose current state must be stored durably and whose transitions trigger external side effects such as sending an email or charging a card?

level: principalimportance: nice to knowfreq 24%

basics

~20 s

Store a stable code for the state, not the object, and rebuild the state object when loading. Decide transitions in memory, commit the new state, then perform outside effects afterwards — retried safely so a repeat does not charge twice.

open as a page