Home › Low-Level Design › State Pattern
S

State Pattern

The State pattern lets an object change its behavior when its internal state changes, as if it quietly swapped itself for a different class at runtime. I reach for it the moment an object starts acting like a tiny machine: one object, a handful of modes, and a pile of rules about what you can and can't do in each mode. If you can draw it as boxes and arrows, it's almost certainly a State problem.

Low-Level DesignMedium10 min read

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. Intent. Behavior changes with mode, so you model each mode as its own class instead of branching on a status field.
  2. When to use. You keep writing the same switch on a status enum across several methods, and transitions have real rules.
  3. Structure. A context delegates every call to its current state object, and each state decides what it does and which state comes next.
  4. States own transitions. The context never asks "what mode am I in." Polymorphism replaces the conditional, and the back-reference to the context is what lets a state move the machine along.
  5. The classic gotcha. People leak the branching back into the context (if current instanceof HasCoinState), which rebuilds the exact if-ladder they were trying to kill.

1The problem it solves#

Start with the pain, because the pattern only makes sense once you feel it.

Picture a vending machine with four modes: idle, coin inserted, item selected, dispensing. Now write insertCoin(), selectItem(), and dispense(). Without the pattern, you get this.

void insertCoin() {
    if (state == IDLE) {
        state = HAS_COIN;
    } else if (state == HAS_COIN) {
        throw new IllegalStateException("coin already in");
    } else if (state == DISPENSING) {
        throw new IllegalStateException("wait, dispensing");
    } else if (state == SOLD_OUT) {
        refund();
    }
    // ...and the same ladder again in selectItem(), and dispense()
}

Every method grows the same if-ladder over the same enum, and the cost shows up three ways.

  • Change amplification. Add one state and you now edit three methods instead of one.
  • Silent bugs. Miss a branch and the machine accepts a coin while dispensing, and nothing throws.
  • Smeared logic. Everything a coin-holding machine can do is scattered across all three methods, so no single file tells the full story.
The smell that should set off alarms

This is the classic "big switch on a status field," and it rots fast. I have watched this exact ladder grow to seven states and nine methods before anyone called it unmaintainable. If you start an LLD answer by typing a status enum and a switch, that is your own cue to stop and model states as classes.

What the State pattern does about it:

  • Pulls each mode into its own class, so the behavior for HAS_COIN lives in one file.
  • Makes transitions explicit instead of implied by which branch you fell into.
  • Replaces the giant conditional with a single method call.

2The idea (mental model)#

Here is the model I use at the whiteboard.

  • Context. The thing the outside world holds, like the vending machine.
  • State objects. One per mode. The context delegates every request to the current one.
  • The hand-off. The state does the work, and it decides which state comes next.

Two sentences that make it click:

  • The context doesn't know how to behave. It only knows who it currently delegates to.
  • Each state knows exactly how to behave in that mode, and which state comes next.
The one line that earns the point

Instead of the machine asking "what mode am I in" on every call, it says "current state, you handle this." Polymorphism replaces the conditional. The if-ladder you would otherwise write becomes the dynamic dispatch table the JVM already gives you for free, with O(1) lookup instead of walking a chain of comparisons.

This is two SOLID principles made concrete:

  • Open/Closed. Adding a new mode means writing a new class, not editing existing ones.
  • Single Responsibility. One class per mode, one reason to change each.
Pause before you read on: Strategy and State look structurally identical, both delegate to a swappable object behind an interface. So what is the actual difference?
Reveal the answer
Intent, and interviewers love this comparison. In Strategy, the client picks the algorithm and it stays put for the lifetime of the object. In State, the objects swap themselves out as a reaction to events. State is Strategy that reprograms itself. Saying this unprompted shows you understand intent, not just shape.

3Structure#

Three moving parts, and one detail people drop.

  • State (an interface). Declares the actions that can happen, like insertCoin() and dispense().
  • Concrete states (IdleState, HasCoinState, and so on). Each implements the interface and holds a reference back to the context so it can trigger the next transition.
  • Context (VendingMachine). Holds the current state, forwards calls to it, and exposes a setState() so states can move it along.
From the interviewer's chair

The back-reference from state to context is the part people forget. A state needs to say "now become the next state," and the only way to do that is to call back into the context. No back-reference, no transition. When a candidate draws the class diagram without the state-to-context arrow, I know they will stall the moment I ask how the machine actually moves. Draw that arrow first.

classDiagram class VendingMachine { -State current -int itemCount +insertCoin() void +selectItem() void +dispense() void +setState(s) void } class State { <> +insertCoin() void +selectItem() void +dispense() void } class IdleState class HasCoinState class DispensingState class SoldOutState State <|.. IdleState State <|.. HasCoinState State <|.. DispensingState State <|.. SoldOutState VendingMachine o-- State IdleState --> VendingMachine HasCoinState --> VendingMachine
Notice the two kinds of arrows. The context holds one State (composition), and every concrete state points back at the context. That back-pointer is the whole transition mechanism, not decoration.

And the runtime flow for a happy-path purchase:

sequenceDiagram participant Client participant VM as VendingMachine participant Idle as IdleState participant HasCoin as HasCoinState Client->>VM: insertCoin() VM->>Idle: insertCoin() Idle->>VM: setState(HasCoinState) Client->>VM: selectItem() VM->>HasCoin: selectItem() HasCoin->>VM: setState(DispensingState) Client->>VM: dispense() VM->>VM: delegate, release item, setState(IdleState)
The client only ever talks to the context. Each call is forwarded to whatever state is current, and the state is the thing that calls setState back. The client never names a state class, which is exactly the encapsulation you want.

4Implementation in Java#

Keep it tight: the interface, the context, and two concrete states so the transition mechanics are visible. The rest follow the same shape.

interface State {
    void insertCoin();
    void selectItem();
    void dispense();
}

class VendingMachine {
    private State idle, hasCoin, dispensing, soldOut;
    private State current;
    private int itemCount;

    VendingMachine(int itemCount) {
        this.itemCount = itemCount;
        this.idle = new IdleState(this);
        this.hasCoin = new HasCoinState(this);
        this.dispensing = new DispensingState(this);
        this.soldOut = new SoldOutState(this);
        this.current = itemCount > 0 ? idle : soldOut;
    }

    // the context just forwards. no branching here.
    public void insertCoin()  { current.insertCoin(); }
    public void selectItem()  { current.selectItem(); }
    public void dispense()    { current.dispense(); }

    void setState(State s) { this.current = s; }

    State idle()       { return idle; }
    State hasCoin()    { return hasCoin; }
    State dispensing() { return dispensing; }
    State soldOut()    { return soldOut; }

    int itemCount() { return itemCount; }
    void releaseItem() { itemCount--; }
}

Now two states. Notice how each one owns only its own rules.

class IdleState implements State {
    private final VendingMachine vm;
    IdleState(VendingMachine vm) { this.vm = vm; }

    public void insertCoin() {
        System.out.println("coin accepted");
        vm.setState(vm.hasCoin());
    }
    public void selectItem() { System.out.println("insert a coin first"); }
    public void dispense()   { System.out.println("insert a coin first"); }
}

class HasCoinState implements State {
    private final VendingMachine vm;
    HasCoinState(VendingMachine vm) { this.vm = vm; }

    public void insertCoin() { System.out.println("coin already inserted"); }

    public void selectItem() {
        System.out.println("item selected");
        vm.setState(vm.dispensing());
        vm.dispense(); // trigger the dispense immediately
    }
    public void dispense() { System.out.println("select an item first"); }
}

And the state that does the real work and loops back:

class DispensingState implements State {
    private final VendingMachine vm;
    DispensingState(VendingMachine vm) { this.vm = vm; }

    public void insertCoin() { System.out.println("wait, dispensing"); }
    public void selectItem() { System.out.println("wait, dispensing"); }

    public void dispense() {
        vm.releaseItem();
        System.out.println("item dispensed");
        vm.setState(vm.itemCount() > 0 ? vm.idle() : vm.soldOut());
    }
}

Three choices worth defending out loud, because an interviewer will probe each:

  • Reuse the state objects (flyweight). I build the four states once in the constructor and reuse them, because they are stateless: they hold only a final reference to the context. Four long-lived instances no matter how many coins go in. If a state had to carry per-visit data (say a timeout count), I would create a fresh instance per transition and accept the garbage.
  • The context never checks its mode. There is not a single if on current anywhere in the context. That absence of branching is the whole point, and it is the first thing I point to when asked "how is this better than the enum?"
  • Edge cases live in the graph, not in guards. The terminal soldOut case is wired from the constructor: build a machine with zero items and it starts in SoldOutState, not Idle. The state graph holds the edge cases, so they are not scattered across methods as ad-hoc checks.

5A real-world example#

The one I hit most often in real code is an order lifecycle: CREATED -> PAID -> SHIPPED -> DELIVERED, with CANCELLED hanging off the side.

Each state answers the same questions differently:

  • Can you cancel? A CREATED order, yes. A SHIPPED order, no.
  • Can you refund? Only after PAID.
  • Can you ship? Only when PAID and in stock.
The production bug this pattern prevents

Writing that as a status enum plus a wall of if (order.status == PAID && ...) scattered across the service layer is how you ship cancelled orders. I have debugged exactly that: a refund path forgot one status check and a delivered order got refunded twice. Model each status as a state class where OrderState.cancel() either performs the transition or rejects it, and the rules for "what a shipped order allows" live in one file, with the compiler forcing you to answer every action for every state.

You also see this baked into frameworks and products:

  • A TCP connection is a state machine (LISTEN, SYN_SENT, ESTABLISHED, CLOSE_WAIT).
  • A media player (stopped, playing, paused) is the textbook case.
  • Document workflows (draft, in review, published, archived) are the same shape.

Any time a product manager hands you a diagram of boxes and arrows, you are looking at a candidate for State.

6When to use it, and when not to#

Use it when:

  • An object has several clearly named modes, and its behavior differs meaningfully per mode.
  • You keep writing the same switch or if on a status field in multiple methods.
  • Transitions have rules, and you want those rules in one obvious place.

Skip it when:

  • You have two modes and one branching method. A boolean and an if is fine. Four state classes for an on/off switch is architecture cosplay.
  • The states never change behavior, they only change a value. That is just a field.
  • The transition logic is genuinely trivial and unlikely to grow. Don't pay the class-count tax for nothing.

The honest tradeoff:

  • State turns one fat class into several small ones. Twelve states means twelve classes, so more files to open.
  • The transition graph is now spread across all of them, which can make the full picture harder to see at a glance.
  • But each class is small, each reason to change is isolated, and every illegal action is explicit.
  • I take that trade almost every time the behavior-per-state is real, and I pair the classes with a transition diagram in the docs so the graph still lives somewhere whole.

7Common mistakes#

Putting the transition logic back in the context.

  • If the context has if (current instanceof HasCoinState) setState(...), you have rebuilt the conditional you were trying to kill.
  • States decide their own transitions. This is the single most common misuse, and the one an interviewer watches for.

Forgetting the back-reference.

  • Without a handle on the context, a state can't trigger the next transition.
  • Candidates draw the diagram without the state-to-context arrow and then can't explain how the machine moves. Draw that arrow first.

Leaking state objects everywhere.

  • The outside world should only touch the context.
  • If callers pass around HasCoinState directly, your encapsulation is gone and the context is no longer the single entry point.

Giant unreachable methods.

  • Every state implements every action, even the ones that aren't allowed. That is the feature: a DISPENSING state explicitly rejecting insertCoin() is documentation the compiler enforces.
  • If you throw the same exception in twenty methods, pull an abstract base class with sensible "not allowed here" defaults and override only what each state permits. Same guarantee, less boilerplate.

Mutable shared state objects.

  • Reuse singleton state instances but store per-request data on them and you get cross-talk under concurrency: two threads sharing one HasCoinState instance stomp each other's data.
  • Keep reusable states stateless, or create fresh ones per transition.

8Where it shows up in interviews#

This is a bread-and-butter LLD prompt. The classics: a vending machine, a parking lot gate, a traffic light, an ATM, an elevator, an order-processing system. All of them are state machines wearing a different hat.

What interviewers actually watch for:

  • Do you spot the state machine? Starting with a status enum and a switch is your cue to say "I'll model each state as a class."
  • Can you draw the states and transitions cleanly? A quick box-and-arrow diagram before any code earns a lot of trust.
  • Do states own their transitions, or did you cheat and branch in the context?
  • How do you handle invalid actions? "Inserting a coin while dispensing" is the edge case a good interviewer pokes at. Have an answer.
  • Can you name the near-neighbor? Say "this is structurally Strategy, but the objects swap themselves on events" before they ask.

My advice for the room: name the states out loud, draw the transition arrows, then write the interface and one interesting state in full. You don't need all six classes to prove you understand it. Show the delegation, show one real transition, and talk through how invalid actions get rejected. That is the whole pattern, and it usually clears the bar in 20 minutes.

Follow-ups the interviewer will ask (and your answers)
"How is this actually better than an enum and a switch?"
The branching disappears. The context has no if on its mode at all, it just forwards to the current state and lets dynamic dispatch pick the behavior. Adding a mode is a new class (Open/Closed), not an edit to three existing methods, and every rule for one mode lives in one file instead of being smeared across every method.
"This looks exactly like Strategy. What's the difference?"
Structurally identical: both delegate to a swappable object behind an interface. The difference is intent. In Strategy the client picks the algorithm and it stays put. In State the objects swap themselves out in reaction to events. State is Strategy that reprograms itself.
"Where should the transition logic live?"
Inside the states, never in the context. The moment the context does if (current instanceof HasCoinState) setState(...), you have rebuilt the conditional you were trying to delete. Each state knows which state comes next and calls setState on the context through its back-reference.
"Do you create a new state object on every transition?"
Not if the states are stateless, and here they only hold a final reference to the context, so I build the four once in the constructor and reuse them. That is the flyweight move. If a state had to carry per-visit data I would create a fresh instance per transition, because reusing a shared instance with mutable fields causes cross-talk under concurrency.
"Every state has to implement every action, even illegal ones. Isn't that waste?"
No, it is the feature. A DISPENSING state explicitly rejecting insertCoin is documentation the compiler enforces. If the same rejection repeats across many methods, I pull an abstract base class with "not allowed here" defaults and override only what each state permits, which keeps the guarantee without the boilerplate.

What you can now explain in the room

  • Why an object with named modes and per-mode behavior is a State problem, not an enum-and-switch problem.
  • How the context delegates to a current state and why it holds no branching at all.
  • Why states own their own transitions, and how the back-reference to the context makes that work.
  • The exact misuse that fails the pattern: branching on the state type back inside the context.
  • When State is worth the class count and when a boolean and an if is the right call instead.
  • How State differs from Strategy in intent despite looking identical in structure.

Next, compare this with the Strategy pattern (same skeleton, different intent) and try the classic Vending Machine and Elevator LLD problems to practice turning a transition diagram into state classes.

Get a prep plan built around your State Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your State Pattern prep plan is on the way.