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?
answer
- object appears to change its class
- one class per state, no if/else inside
- context delegates to currentState
- transition = swap the reference
- states usually stateless + shared
basics
~20 sIt 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 sState 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 linesinterface 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
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.
Add where data lives (context, states stateless and shareable), who performs transitions, and the Open/Closed benefit versus the class-count cost.
Discuss entry/exit hooks, a base state implementing the reject-everything default, illegal-transition policy, centralizing transitionTo, and persistence via a discriminator.
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.