intermediate~2h

State Pattern

Learn how to replace scattered switch statements with self-contained state classes, so an object's behavior changes cleanly as its internal state changes.

Learning objectives

  • Recognize when a class's behavior is fragmented across duplicate switch or if statements tied to one field
  • Extract each behavioral case into its own class implementing a shared interface
  • Distinguish caller-driven state assignment from self-transitioning state machines
  • Decide when sharing stateless state instances is safe and when it becomes a bug

◆ Story

A traffic light reacts to the exact same event — a timer firing — in three completely different ways, depending only on its own current color. Green switches to yellow. Yellow switches to red. Red switches back to green. Nothing outside the light decides which of these happens; the light's own current condition is what determines its next behavior.

That is the core idea behind the State pattern: an object's behavior changes based on its own internal condition, without any external code needing to check what that condition currently is before deciding what to do.

A navigation app needs the same shape of problem solved. Its DirectionService exposes two operations, getEta() and getDirections(), and the actual answer to both depends entirely on which transport mode the user picked — walking, cycling, car, or train. Ask for an ETA while "cycling" is selected and you get a different number than if "car" were selected, using the exact same method call. The transport mode plays the same role the traffic light's color plays: one field, quietly controlling how every method behaves.

The obvious first implementation checks that field with a conditional, inside every method that needs to behave differently. It is worth building that version first, and watching exactly where it starts to hurt as the app grows.

The obvious way to make getEta() and getDirections() depend on the transport mode is to store the mode as an enum field and branch on it, inside each method, with a switch statement.

This works. DirectionService(TransportMode.CYCLING).getEta() returns 5, getDirections() returns "Take the bike lane," and adding a setMode() method lets the same service object be reused for a different mode later. For four transport modes and two methods, this is entirely readable code — nobody would flag it in review.

The trouble shows up the moment a fifth mode needs to be added, or a third method. Every method that cares about transport mode needs the exact same four-way (soon five-way) branch, copy-pasted in. getEta() and getDirections() already duplicate the same switch structure once; a real navigation service would also need fare estimates, calorie estimates, and route polylines, and every one of them would need this same switch statement written in again.

Worse, adding a mode means going back into every existing method and adding a new case to each one — getEta(), getDirections(), and anything written afterward. It is entirely possible to correctly add a case to three of four methods and simply forget the fourth, and nothing about the code flags that as wrong; it just silently keeps hitting the old default or throwing on the new mode wherever the case was missed.

▲ Common mistake

This is a direct violation of the Open/Closed Principle: supporting a new mode requires modifying code that already worked and was already tested, rather than only adding new code alongside it.

💻 Code example

enum TransportMode { WALKING, CYCLING, CAR, TRAIN } class DirectionService { private TransportMode mode; DirectionService(TransportMode mode) { this.mode = mode; } void setMode(TransportMode mode) { this.mode = mode; } int getEta() { switch (mode) { case WALKING: return 10; case CYCLING: return 5; case CAR: return 2; case TRAIN: return 3; default: throw new IllegalStateException(); } } String getDirections() { // the SAME four-way branch, again switch (mode) { case WALKING: return "Directions for walking"; case CYCLING: return "Take the bike lane"; case CAR: return "Directions for driving"; case TRAIN: return "Directions for the nearest station"; default: throw new IllegalStateException(); } } } // Adding a fifth mode means editing getEta() AND getDirections() AND // every method written after this one -- and it is easy to update three // out of four methods correctly and never notice the fourth was missed.

Look closely at what actually belongs together. "Walking" behavior — its ETA of 10 minutes, its directions string — is one coherent idea, not something that should live as one case scattered across several unrelated switch statements. The fix is to give each transport mode its own class, holding all of its own behavior, with every mode implementing one shared interface so DirectionService can treat all of them identically.

That is the State pattern: extract each behavioral case into its own class implementing a shared contract, and have the object that used to branch on a field instead hold a reference to the current state object and simply delegate every call to it.

The shared contract, TransportationMode, declares both operations every mode must be able to answer. Each old switch case becomes its own small class implementing that interface — Walking, Cycling, Car, Train — each one holding just its own ETA and its own directions string, with nothing about any other mode visible inside it.

DirectionService itself changes shape completely. Instead of a TransportMode enum field and two switch statements, it holds a single TransportationMode reference and delegates every call to whichever object that reference currently points at. getEta() becomes a single line — return mode.getEta(); — with zero knowledge of which modes exist or how many there are. The class that used to be the one place with a wrong-mode risk no longer contains a single conditional.

💻 Code example

interface TransportationMode { int getEta(); String getDirections(); } class Walking implements TransportationMode { public int getEta() { return 10; } public String getDirections() { return "Directions for walking"; } } class Cycling implements TransportationMode { public int getEta() { return 5; } public String getDirections() { return "Take the bike lane"; } } // Car and Train follow the identical shape -- each mode's ETA and // directions logic now live together, in one place, instead of being // scattered across several switch statements. class DirectionService { private TransportationMode mode; // holds the CURRENT mode object, not an enum DirectionService(TransportationMode mode) { this.mode = mode; } void setMode(TransportationMode mode) { // swap the active mode object this.mode = mode; } int getEta() { return mode.getEta(); } // pure delegation, no switch String getDirections() { return mode.getDirections(); } // same delegation, zero duplication }

The real test of this design is adding a new transport mode and seeing exactly what has to change.

First, confirm the refactored version behaves identically to the original. Constructing DirectionService with a Cycling object and calling getEta() returns 5, exactly as before; calling setMode(new Car()) swaps the active mode at runtime, and the very next getEta() call returns 2, using the same service object.

Now add a fifth mode: flights. In the switch-statement version, this meant editing getEta(), editing getDirections(), and editing every future method the same way. In this version, it means writing exactly one new class — Airplane, implementing TransportationMode — and nothing else. DirectionService is not opened, not recompiled around new logic, not touched at all.

The failure mode from the naive version, where a developer might correctly update three methods and forget a fourth, is now structurally impossible: neither getEta() nor getDirections() has any way to even reference which modes exist, so there is nothing left to forget. This is the practical payoff of the State pattern — the set of possible states can grow indefinitely without the object that delegates to them ever needing a matching edit.

💻 Code example

public class Main { public static void main(String[] args) { DirectionService service = new DirectionService(new Cycling()); System.out.println(service.getEta()); // 5 System.out.println(service.getDirections()); // "Take the bike lane" service.setMode(new Car()); // swapped, at runtime -- same service object System.out.println(service.getEta()); // 2 } } // Adding flight support -- the entire diff, DirectionService untouched: class Airplane implements TransportationMode { public int getEta() { return 90; } public String getDirections() { return "Directions to the nearest airport"; } } // service.setMode(new Airplane());

▲ Edge case: states that decide their own next state

Every mode in the example above is switched from the outside, by whoever calls setMode(). The State pattern has a more distinctive form where a state decides, on its own, to transition the context to a different state entirely — which is actually closer to the traffic light from the opening story, since nothing external tells a green light to turn yellow. A media player shows this well: a StoppedState handling pressPlay() can call player.setState(new PlayingState()) itself, so the caller never chooses PlayingState directly — it just says "play," and the current state decides what happens next. Both shapes are legitimately the State pattern; the transport-mode version above is the simpler, more common shape in everyday application code, while self-transitioning states are what the pattern is built for once "what comes next" genuinely depends on internal rules rather than an external caller's choice.

▲ Edge case: undefined transitions need a deliberate answer

In the self-transitioning version, what should PlayingState.pressStop() followed immediately by a second pressStop() do? Every state's method needs to explicitly decide what happens for every trigger it might receive, including triggers that do not make sense from that particular state. Doing nothing is a perfectly valid choice — but it has to be a decision written into that state's code, not a gap nobody noticed until a bug report came in.

Stateless mode objects like Walking and Cycling hold no data specific to any one DirectionService — a single shared Walking instance could safely serve every DirectionService in an entire application at once, avoiding repeated allocation for something that never changes. That stops being safe the moment a state needs to hold data specific to one particular context, such as how long the service has been in that mode — at that point, sharing one instance across multiple contexts becomes a real, hard-to-trace bug, since two unrelated services would end up reading and overwriting each other's data through the same shared object.

💻 Code example

interface PlayerState { void pressPlay(MediaPlayer player); } class StoppedState implements PlayerState { public void pressPlay(MediaPlayer player) { System.out.println("Starting playback"); player.setState(new PlayingState()); // the STATE decides the transition } } class PlayingState implements PlayerState { public void pressPlay(MediaPlayer player) { // already playing -- deliberately does nothing } } class MediaPlayer { private PlayerState state = new StoppedState(); void setState(PlayerState state) { this.state = state; } void pressPlay() { state.pressPlay(this); } // caller just says "play" }

◆ Under the hood

The State pattern shows up anywhere an object's entire set of behaviors legitimately changes based on which phase or mode it is currently in.

  • Order processing systems — an order moving through PLACED to PAID to SHIPPED to DELIVERED, where each state only permits certain next transitions, and the same "cancel" action behaves completely differently (or is rejected outright) depending on which state the order is currently in.
  • UI components — a button rendering and responding to clicks differently depending on whether it is enabled, disabled, or in a loading state.
  • Vending machines — waiting for payment, dispensing product, or out of stock, each state responding to the exact same "press button" event in an entirely different way.
  • TCP connections — listening, connecting, connected, or closed, where the same "send data" call is either handled normally or rejected outright, purely based on the connection's current state.
  • Workflow and approval engines — a document in draft, in review, or approved, where the set of valid next actions is entirely determined by its current status.

In each case, the alternative to State is a field checked repeatedly across many methods — exactly the switch-statement pattern this chapter replaced.

Q: What problem does the State pattern actually solve? : It stops the same "what happens in this mode" logic from being duplicated across every method of a class — each state's full behavior lives together in one class, instead of being scattered as one case inside several different switch statements.

Q: In the direction-service example, what changes inside DirectionService when a new transport mode is added? : Nothing. Adding a mode means writing one new class implementing TransportationMode — DirectionService has no switch statement and no knowledge of which modes exist, so there is nothing in it left to edit.

Q: What is the difference between the caller-driven and self-transitioning forms of the State pattern? : In the caller-driven form, external code decides which state is active by calling something like setMode(). In the self-transitioning form, the current state object itself decides and triggers the next state as part of handling an event, without the caller choosing the next state directly.

Q: When is it safe to share one state instance across multiple context objects? : When the state is stateless — it holds no data specific to any one context. The moment a state needs to track something specific to a particular object, such as how long it has been active, sharing one instance across contexts becomes a bug.

Q: Why must a self-transitioning state explicitly handle triggers that do not make sense from its current state? : Because leaving a trigger unhandled is an easy way to introduce a silent bug — every state needs a deliberate answer for every event it might receive, even if that answer is to do nothing.

Want a visual for this concept?

Generate a diagram tailored to “State Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Mediator Pattern← Back to all Low-Level Design & Design Patterns chapters