State Pattern
When an Object's Behavior Changes Based on What It Remembers
LinkedIn Hook
Here is a question that separates mid-level candidates from senior ones:
"What is the difference between the State pattern and the Strategy pattern?"
Most developers have used state machines without naming them. They have written giant
if/elseblocks that check a status field and behave differently in each branch. They have seen those blocks grow to 200 lines and become unmaintainable.The State pattern solves exactly that problem. It extracts each "mode" into its own class. The object delegates behavior to the current state object instead of branching on a field. When the state changes, you swap the object. The conditionals disappear.
Strategy and State look nearly identical in structure. The difference is in intent and lifecycle. Interviewers know this. They use it to test whether you understand patterns at a conceptual level or just at a syntax level.
Lesson 8.6 covers the full pattern, a working vending machine, a traffic light, and a direct State vs Strategy comparison table you can use in any interview.
Read the full lesson -> [link]
#OOP #JavaScript #DesignPatterns #InterviewPrep #StateMachine
What You'll Learn
- What the State pattern is and the core problem it solves (the
if/elseexplosion) - How a state machine works conceptually and how to map it to objects and classes
- A fully working vending machine implementation in JavaScript using the State pattern
- A traffic light example showing automatic state transitions
- A direct State vs Strategy comparison table covering intent, lifecycle, and transition ownership
- When to reach for the State pattern and when a simple status field is enough
- The most common interview trap: explaining why State and Strategy are structurally similar but conceptually opposite
The Analogy That Makes It Click
Think about a traffic light.
A traffic light is always in exactly one state at a time: Red, Yellow, or Green. In each state, it behaves differently. Red stops traffic. Green allows it. Yellow warns of the transition coming. You cannot be Red and Green at the same time.
The light does not consult a large decision tree every time a car approaches. It does not ask "what is my current status, and given that status, what should I do?" It simply behaves as what it currently is. When a timer fires, it transitions to the next state, and then it behaves as that new thing.
That is the State pattern. The object delegates all behavior to a current state object. When something triggers a change, the current state object is swapped out for the next one. The object itself stays the same. Only the delegate changes.
[INTERNAL-LINK: behavioral patterns overview → Lesson 8.1: Introduction to Design Patterns]
What Is the State Pattern?
The State pattern is a behavioral design pattern that lets an object alter its behavior when its internal state changes. Instead of storing a status string and branching on it throughout the codebase, each state is encapsulated in its own class. The main object holds a reference to the current state object and delegates method calls to it.
The Gang of Four formally defined the State pattern in "Design Patterns: Elements of Reusable Object-Oriented Software" (1994). Their motivation was the same problem every developer hits when writing complex workflow objects: a single class accumulates dozens of conditionals all checking the same status variable. Adding a new state means editing every conditional in the class. That is fragile, hard to test, and easy to break.
The State pattern makes each state a closed, independent unit. Adding a new state means adding a new class, not editing existing ones. This satisfies the Open/Closed Principle from SOLID.
[INTERNAL-LINK: Open/Closed Principle in SOLID → Lesson 7.x: SOLID Principles]
What Is a State Machine?
A state machine is a model of behavior with a fixed set of states, a set of possible transitions between those states, and rules about what triggers each transition. At any moment, the machine is in exactly one state. Inputs or events cause it to transition to another state or stay in the same one.
State machines appear everywhere in software: order workflows (Pending, Confirmed, Shipped, Delivered, Cancelled), user sessions (LoggedOut, LoggingIn, LoggedIn, Locked), network connections (Closed, Listening, Established, Closing), and UI components (Idle, Loading, Success, Error).
The State pattern is one clean way to implement a state machine in OOP. The machine is the context object. Each state is a class. Transitions happen when a state method tells the context to switch to a different state object.
[CHART: State diagram - vending machine states (Idle, HasCoin, Dispensing, OutOfStock) connected by labeled transition arrows - draw manually or describe to Napkin AI]
Code Example 1 — Vending Machine (State Pattern, Full Implementation)
This is the classic State pattern example. A vending machine has four states: Idle (waiting for a coin), HasCoin (coin inserted, waiting for selection), Dispensing (dispensing the item), and OutOfStock (no items left). The same method, insertCoin() or selectProduct(), behaves completely differently depending on the current state.
// Each state is a class that implements the same interface
// insertCoin, selectProduct, dispense, refund
class IdleState {
constructor(machine) {
this.machine = machine;
}
insertCoin() {
console.log("Coin inserted. Select a product.");
// Transition to the next state
this.machine.setState(this.machine.hasCoinState);
}
selectProduct() {
console.log("Please insert a coin first.");
}
dispense() {
console.log("Please insert a coin first.");
}
refund() {
console.log("No coin to refund.");
}
}
class HasCoinState {
constructor(machine) {
this.machine = machine;
}
insertCoin() {
console.log("A coin is already inserted. Refunding the extra coin.");
}
selectProduct() {
if (this.machine.getStock() === 0) {
console.log("Out of stock. Refunding your coin.");
this.machine.setState(this.machine.outOfStockState);
return;
}
console.log("Product selected. Dispensing...");
// Transition to dispensing state
this.machine.setState(this.machine.dispensingState);
}
dispense() {
console.log("Select a product first.");
}
refund() {
console.log("Coin refunded.");
// Transition back to idle
this.machine.setState(this.machine.idleState);
}
}
class DispensingState {
constructor(machine) {
this.machine = machine;
}
insertCoin() {
console.log("Please wait. Dispensing in progress.");
}
selectProduct() {
console.log("Please wait. Dispensing in progress.");
}
dispense() {
this.machine.reduceStock();
if (this.machine.getStock() === 0) {
console.log("Item dispensed. Machine is now out of stock.");
// Transition to out-of-stock state
this.machine.setState(this.machine.outOfStockState);
} else {
console.log("Item dispensed. Ready for next customer.");
// Transition back to idle
this.machine.setState(this.machine.idleState);
}
}
refund() {
console.log("Cannot refund while dispensing.");
}
}
class OutOfStockState {
constructor(machine) {
this.machine = machine;
}
insertCoin() {
console.log("Out of stock. Coin returned.");
}
selectProduct() {
console.log("Out of stock.");
}
dispense() {
console.log("Out of stock. Nothing to dispense.");
}
refund() {
console.log("No coin inserted.");
}
}
// Context class — holds reference to current state, delegates all calls
class VendingMachine {
constructor(initialStock) {
// Create all possible state objects upfront
this.idleState = new IdleState(this);
this.hasCoinState = new HasCoinState(this);
this.dispensingState = new DispensingState(this);
this.outOfStockState = new OutOfStockState(this);
this.stock = initialStock;
// Set initial state based on stock level
this.currentState = initialStock > 0 ? this.idleState : this.outOfStockState;
}
// Delegate all public actions to the current state object
insertCoin() {
this.currentState.insertCoin();
}
selectProduct() {
this.currentState.selectProduct();
}
dispense() {
this.currentState.dispense();
}
refund() {
this.currentState.refund();
}
// State management methods — called by state objects to trigger transitions
setState(newState) {
this.currentState = newState;
}
getStock() {
return this.stock;
}
reduceStock() {
this.stock = Math.max(0, this.stock - 1);
}
}
// --- Running the vending machine ---
const machine = new VendingMachine(2);
console.log("--- Customer 1 ---");
machine.insertCoin(); // "Coin inserted. Select a product."
machine.selectProduct(); // "Product selected. Dispensing..."
machine.dispense(); // "Item dispensed. Ready for next customer."
console.log("\n--- Customer 2 (last item) ---");
machine.insertCoin(); // "Coin inserted. Select a product."
machine.selectProduct(); // "Product selected. Dispensing..."
machine.dispense(); // "Item dispensed. Machine is now out of stock."
console.log("\n--- Customer 3 (machine empty) ---");
machine.insertCoin(); // "Out of stock. Coin returned."
machine.selectProduct(); // "Out of stock."
console.log("\n--- Invalid sequence ---");
const machine2 = new VendingMachine(5);
machine2.selectProduct(); // "Please insert a coin first."
machine2.dispense(); // "Please insert a coin first."
machine2.insertCoin();
machine2.insertCoin(); // "A coin is already inserted. Refunding the extra coin."
machine2.refund(); // "Coin refunded."
[IMAGE: Vending machine state diagram showing four circles (Idle, HasCoin, Dispensing, OutOfStock) with labeled arrows between them - search terms: "state machine diagram vending machine"]
[PERSONAL EXPERIENCE]: The most telling sign that State pattern is needed is when you find yourself adding the same if (this.status === "X") guard to five different methods. Each new status you add requires touching every method again. The State pattern eliminates that by making the guard implicit: the wrong state simply does not have the behavior.
Code Example 2 — Traffic Light (Automatic Transitions)
The traffic light example shows a key variation: states that transition themselves on a timer, without any external trigger calling setState. Each state schedules its own successor.
class RedState {
constructor(light) {
this.light = light;
this.duration = 5000; // Red for 5 seconds
}
enter() {
console.log("RED - Stop");
// State schedules its own transition after the duration
setTimeout(() => {
this.light.setState(this.light.greenState);
}, this.duration);
}
}
class GreenState {
constructor(light) {
this.light = light;
this.duration = 4000; // Green for 4 seconds
}
enter() {
console.log("GREEN - Go");
setTimeout(() => {
this.light.setState(this.light.yellowState);
}, this.duration);
}
}
class YellowState {
constructor(light) {
this.light = light;
this.duration = 1500; // Yellow for 1.5 seconds
}
enter() {
console.log("YELLOW - Slow down");
setTimeout(() => {
this.light.setState(this.light.redState);
}, this.duration);
}
}
class TrafficLight {
constructor() {
this.redState = new RedState(this);
this.greenState = new GreenState(this);
this.yellowState = new YellowState(this);
// Start at red
this.currentState = this.redState;
}
start() {
// Call enter() on the initial state to kick off the cycle
this.currentState.enter();
}
setState(newState) {
this.currentState = newState;
// Immediately trigger the new state's entry behavior
this.currentState.enter();
}
}
// --- Running the traffic light ---
// Output (timed):
// RED - Stop (at 0s)
// GREEN - Go (at 5s)
// YELLOW - Slow down (at 9s)
// RED - Stop (at 10.5s)
// ... (cycles indefinitely)
const light = new TrafficLight();
light.start();
This example highlights how the State pattern handles self-driving transitions cleanly. The context, TrafficLight, has no timer logic at all. Each state class owns its own duration and knows which state comes next. To change the cycle, you only change the relevant state class.
Code Example 3 — Without the Pattern (The Problem It Solves)
Seeing the problem first makes the solution more meaningful. Here is a stripped-down vending machine written without the State pattern, using the status-field approach most developers reach for first.
class VendingMachineNaive {
constructor(stock) {
this.stock = stock;
// All state lives in one field — drives all branching below
this.status = stock > 0 ? "idle" : "out_of_stock";
}
insertCoin() {
// Every method checks the same field
if (this.status === "idle") {
console.log("Coin inserted. Select a product.");
this.status = "has_coin";
} else if (this.status === "has_coin") {
console.log("Already has a coin. Returning extra.");
} else if (this.status === "dispensing") {
console.log("Dispensing in progress. Please wait.");
} else if (this.status === "out_of_stock") {
console.log("Out of stock. Returning coin.");
}
// Add a new state? Edit this method. And every other method.
}
selectProduct() {
if (this.status === "idle") {
console.log("Insert a coin first.");
} else if (this.status === "has_coin") {
if (this.stock === 0) {
console.log("Out of stock.");
this.status = "out_of_stock";
return;
}
console.log("Product selected. Dispensing...");
this.status = "dispensing";
} else if (this.status === "dispensing") {
console.log("Please wait.");
} else if (this.status === "out_of_stock") {
console.log("Out of stock.");
}
// The if/else block grows by one branch every time you add a state
}
// ... dispense() and refund() each have their own copy of the same if/else tree
}
[UNIQUE INSIGHT]: The status-field approach is not wrong for simple cases. If an object has two states and three methods, the if/else approach is fine — it is less code and easier to read. The State pattern introduces four or more classes for a problem that one class could solve. Use the pattern when the number of states times the number of methods makes the branching genuinely hard to follow, or when new states are being added frequently and you want to contain that growth.
Code Example 4 — State with a Shared Interface (Explicit Contract)
In JavaScript, interfaces are not enforced at runtime. But documenting the expected interface in a base class or JSDoc comment makes the contract explicit and catches missing method implementations during code review.
// Base class acts as an interface contract for all states
// Every concrete state must implement these four methods
class VendingMachineState {
constructor(machine) {
if (new.target === VendingMachineState) {
throw new Error("VendingMachineState is abstract. Do not instantiate directly.");
}
this.machine = machine;
}
insertCoin() {
throw new Error(`${this.constructor.name} must implement insertCoin()`);
}
selectProduct() {
throw new Error(`${this.constructor.name} must implement selectProduct()`);
}
dispense() {
throw new Error(`${this.constructor.name} must implement dispense()`);
}
refund() {
throw new Error(`${this.constructor.name} must implement refund()`);
}
}
// Concrete states extend the base — they inherit the guard and must override all methods
class ReadyState extends VendingMachineState {
insertCoin() {
console.log("Coin accepted.");
this.machine.setState(this.machine.selectedState);
}
selectProduct() {
console.log("Please insert a coin first.");
}
dispense() {
console.log("Please insert a coin first.");
}
refund() {
console.log("No coin to refund.");
}
}
// Any state that forgets to implement a method will throw a clear error at runtime
// rather than silently failing or producing a wrong result
[INTERNAL-LINK: abstract classes and enforcing interfaces in JS → Lesson 3.2: Abstract Classes]
State vs Strategy — The Interview Trap
This comparison is the question most interviewers build around once they confirm you know both patterns. State and Strategy look nearly identical structurally. Both involve a context class that holds a reference to a pluggable object and delegates calls to it. The difference is in intent, ownership of transitions, and whether the delegate changes during the object's lifetime.
STATE vs STRATEGY — FULL COMPARISON
---------------------------------------------------------------------------
Feature State Pattern Strategy Pattern
---------------------------------------------------------------------------
Intent An object changes its An object selects one
behavior when its algorithm from a family
internal state changes. of algorithms.
"I am different things "I will do this using
at different times." this particular approach."
Who changes The state objects The client/caller
the current themselves trigger explicitly sets which
delegate? transitions. The context strategy to use. The
rarely does it directly. context does not change
it on its own.
Does the Yes. Transitions are Not typically.
delegate change part of the core Strategy is usually set
during runtime? behavior — that is the once (or rarely changed)
whole point. per operation.
States/Strategies States know about each Strategies are
aware of each other (to trigger independent. A strategy
other? transitions). has no idea what other
strategies exist.
State stored Context holds current Context holds current
where? state. State may also strategy. No transition
hold a reference back mechanism inside the
to the context. strategy itself.
Driving force Internal events trigger External code chooses
for change transitions. The object which algorithm to
manages its own apply based on outside
lifecycle. conditions.
Typical example Vending machine, Sorting algorithm
traffic light, selection, payment
order workflow, method, compression
TCP connection algorithm, route
planning
Core question "What state am I in "Which algorithm
it answers right now, and what should I use to
can I do?" accomplish this task?"
Structural Context + State Context + Strategy
components interface + concrete interface + concrete
state classes strategy classes
Can they be Yes — structurally Yes — structurally
confused? identical. The difference identical. The difference
is always conceptual is always conceptual
and in transition and in who drives
ownership. the change.
---------------------------------------------------------------------------
[IMAGE: Two-column diagram side by side - left labeled "STATE" showing a context box with arrows between state objects labeled with transition triggers, right labeled "STRATEGY" showing a context box with one strategy slot and an external caller swapping the strategy in - search terms: "state pattern vs strategy pattern diagram OOP"]
The one-sentence summary interviewers want to hear:
State: The object knows what state it is in and changes its own delegate as events happen. Strategy: The caller chooses which algorithm the object will use for a task.
When Should You Use the State Pattern?
Use State when all of these are true:
- An object behaves differently depending on a current "mode" or "status."
- The number of states is more than two or three, or new states are added regularly.
- The branching logic (if/else or switch on a status field) is duplicated across multiple methods in the class.
- State transitions are triggered by the object's own behavior, not by the caller.
Do not use State when:
- The object has only two modes and the branching is trivial.
- The "state" is just a single boolean flag with no complex behavior per state.
- Transitions are always driven externally by the caller (that is Strategy's territory).
- The extra classes add complexity without reducing the actual branching burden.
[CHART: Decision flowchart - "Should I use the State pattern?" - starting from "Does behavior change based on an internal status?" branching through number of states, duplicated branching, and transition ownership to yes/no recommendation]
Common Mistakes
-
Putting transition logic in the context instead of the states. If the context class contains a large
if/elseblock deciding which state to switch to next, you have moved the problem rather than solved it. Transition logic belongs inside each state class. The context only callssetState()when a state tells it to. -
Confusing State with Strategy in the interview. The structures are visually identical. The question is always about who drives the change and why. In State, transitions are internal and automatic. In Strategy, the caller picks the algorithm. Practise the one-sentence version: "State changes itself; Strategy is chosen externally."
-
Creating state classes for things that are not states. Not every property variation is a state. A
Userwith arolefield (admin, editor, viewer) might look like a State pattern candidate, but if only one or two methods behave differently and the role rarely changes, a simple permission-check function is cleaner than four state classes. -
Forgetting to initialize the context with a valid starting state. If the context's
currentStateisnullat construction and a method is called immediately, you get a null reference error. Always assign a concrete initial state in the constructor. TheVendingMachineexample above initializes based on stock level — eitheridleStateoroutOfStockState. -
Making states stateless when they need to remember something. A state class can hold its own data (duration, retry count, error message). Treating every state as a pure behavior delegate and then trying to store transition data in the context creates an awkward coupling. If a state needs data specific to that state, put it in the state class.
Interview Questions
Q: What problem does the State pattern solve?
The State pattern solves the "conditional explosion" problem. When an object behaves differently based on an internal status and you model that with if/else or switch statements, every method accumulates one branch per state. Adding a new state means editing every method. The State pattern extracts each state into its own class. Each class handles only its own behavior. Adding a new state means adding a new class, not editing existing ones. This satisfies the Open/Closed Principle.
Q: What is the structural difference between State and Strategy?
Structurally, they are nearly identical: a context class holds a reference to a pluggable object and delegates method calls to it. The difference is conceptual. In Strategy, the caller selects which algorithm the context uses, and the strategy object does not change itself. In State, the state objects trigger their own transitions: a state method tells the context to switch to the next state. The object manages its own lifecycle. Interviewers use this comparison to test whether you understand intent, not just structure.
Q: Who is responsible for triggering state transitions?
In the State pattern, the state objects themselves are responsible. When HasCoinState.selectProduct() is called, it calls this.machine.setState(this.machine.dispensingState) directly. The context class delegates the call and the state class decides what comes next. This is the key distinction from Strategy, where the context or caller makes that decision externally. Some implementations allow the context to manage transitions if the transition rules are simple enough, but the canonical pattern puts this responsibility in the states.
Q: Can the same state object be shared across multiple context instances?
Yes, if the state object holds no instance-specific data. A stateless state object (one that only holds a reference to the context it was constructed with) should not be shared across multiple contexts, because this.machine would point to only one of them. But if you design state objects as pure behavior delegates with no stored reference (passing the context as a method argument instead), they can be shared as flyweights. This is a memory optimization sometimes seen in high-volume state machines.
Q: How does the State pattern relate to SOLID principles?
The State pattern directly supports two SOLID principles. It supports the Open/Closed Principle: adding a new state requires adding a new class, not modifying existing state classes or the context. It supports the Single Responsibility Principle: each state class is responsible for the behavior of exactly one state, nothing more. The context class is responsible for holding and delegating to the current state, nothing more. The original status-field approach violates both: every method in the context is responsible for every state's behavior simultaneously.
Quick Reference Cheat Sheet
STATE PATTERN — QUICK REFERENCE
---------------------------------------------------------------------------
Intent Let an object change its behavior when its internal
state changes. Encapsulate each state as a class.
Structure Context class holds currentState reference
State interface defines the shared method contract
Concrete state classes implement behavior for one state
Transitions triggered by state methods calling
context.setState(nextState)
Key roles
Context - holds a reference to the current state object
- delegates all public calls to currentState
- exposes setState() for state objects to call
- creates all state instances (usually in constructor)
State - implements all methods defined in the state interface
- holds a back-reference to the context (usually)
- calls context.setState() to trigger transitions
- knows about adjacent states (the ones it transitions to)
---------------------------------------------------------------------------
SKELETON (JavaScript)
---------------------------------------------------------------------------
class Context {
constructor() {
this.stateA = new StateA(this);
this.stateB = new StateB(this);
this.currentState = this.stateA; // initial state
}
action() {
this.currentState.action(); // delegate to current state
}
setState(newState) {
this.currentState = newState;
}
}
class StateA {
constructor(ctx) { this.ctx = ctx; }
action() {
console.log("StateA behavior");
this.ctx.setState(this.ctx.stateB); // transition
}
}
class StateB {
constructor(ctx) { this.ctx = ctx; }
action() {
console.log("StateB behavior");
this.ctx.setState(this.ctx.stateA); // transition back
}
}
---------------------------------------------------------------------------
STATE vs STRATEGY — ONE-LINE SUMMARY
---------------------------------------------------------------------------
State: Object changes its own delegate as internal events happen.
Strategy: Caller chooses which algorithm the object will use.
---------------------------------------------------------------------------
WHEN TO USE STATE
---------------------------------------------------------------------------
Use it when:
- Object has 3+ distinct modes with different behavior per mode
- The same if/else block appears in multiple methods
- Transitions are triggered by the object's own behavior
- New states are added frequently
Skip it when:
- Only 2 states and behavior differences are trivial
- The "state" is a simple boolean flag
- Transitions are always set externally by the caller (use Strategy)
- The added classes increase complexity without reducing branching
---------------------------------------------------------------------------
COMMON STATE MACHINE APPLICATIONS
---------------------------------------------------------------------------
Order workflow: Pending -> Confirmed -> Shipped -> Delivered -> Cancelled
User session: LoggedOut -> LoggingIn -> LoggedIn -> Locked
Network socket: Closed -> Listening -> Established -> Closing
Media player: Stopped -> Playing -> Paused -> Buffering
Vending machine: Idle -> HasCoin -> Dispensing -> OutOfStock
Traffic light: Red -> Green -> Yellow -> Red (cyclic)
---------------------------------------------------------------------------
STATE PATTERN vs STRATEGY PATTERN
---------------------------------------------------------------------------
Feature State Strategy
Delegate changes? Yes, automatically Rarely, set externally
Who triggers change? State objects Client / caller code
States aware of Yes (know successors) No (independent)
each other?
Lifecycle Managed by the object Managed by the caller
Core question "What mode am I in?" "Which algorithm do I use?"
---------------------------------------------------------------------------
Previous: Lesson 8.5 - Strategy Pattern Next: End of course
This is Lesson 8.6 of the OOP Interview Prep Course — 8 chapters, 41 lessons.