LESSON 7.2
State
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
15 min read
Real-world analogy
A modern smartphone exhibits the State pattern through its physical buttons. When the phone is in the Unlocked State, pressing the power button turns off the screen, and pressing volume buttons adjusts media sound. When the phone is in the Ringing State from an incoming call, pressing the power button silences the ringer without sleeping, and tapping the screen answers or rejects the call. When the phone is in the Depleted Battery State, pressing the power button simply displays a blinking empty-battery icon. The physical hardware buttons never change, but the phone's behavior changes completely based on its internal state.
The problem
You are designing an automated vending machine controller. The machine supports four primary operations:
insertCoin()ejectCoin()pressButton()dispense()
The machine moves across four discrete states: NO_COIN, HAS_COIN, SOLD_OUT, and DISPENSING.
Without the State pattern, every single operation in the VendingMachine class contains a massive switch or if-else block checking the current state:
// Naive implementation: Sprawling conditionals inside every single method
class VendingMachine {
#state = "NO_COIN";
#inventory = 5;
insertCoin() {
if (this.#state === "NO_COIN") {
this.#state = "HAS_COIN";
console.log("Coin inserted. Press button to select item.");
} else if (this.#state === "HAS_COIN") {
console.log("Coin already inserted! Eject first.");
} else if (this.#state === "SOLD_OUT") {
console.log("Machine sold out. Coin rejected.");
} else if (this.#state === "DISPENSING") {
console.log("Please wait, dispensing in progress.");
}
}
pressButton() {
if (this.#state === "NO_COIN") {
console.log("Insert coin first!");
} else if (this.#state === "HAS_COIN") {
this.#state = "DISPENSING";
this.dispense();
} else if (this.#state === "SOLD_OUT") {
console.log("Sold out!");
} else if (this.#state === "DISPENSING") {
console.log("Already dispensing.");
}
}
dispense() {
// Another 4-way switch statement duplicated here...
}
}public class Example {
static class VendingMachine {
enum State { NO_COIN, HAS_COIN, DISPENSING, SOLD_OUT }
State state = State.NO_COIN;
public void insertCoin() {
switch (state) {
case NO_COIN -> { state = State.HAS_COIN; System.out.println("Coin accepted"); }
Adding a fifth state (such as MAINTENANCE_MODE or PROCESSING_CREDIT_CARD) requires modifying every method in the class, risking regressions in unrelated states and violating the Open/Closed Principle.
The solution
The State pattern extracts all state-specific behavior into a hierarchy of separate State classes. The context holds a reference to its current state object and delegates all actions to it. When an event occurs, the state object transitions the context to a new state instance.
- Context (
VendingMachine): Maintains the current state instance and public interface. - State (
VendingState): Interface declaring all available actions (insertCoin,pressButton,ejectCoin,dispense). - Concrete States (
NoCoinState,HasCoinState,SoldOutState): Implements state-specific responses and triggers transitions.
Step-by-step implementation
Step 1: Define the state interface
Declare all methods that the context can invoke.
Step 2: Implement the context class
The context delegates operations to its current state and provides a package-private method to swap states.
Step 3: Implement concrete state classes
Concrete states encapsulate valid actions and transition the context to next states.
The complete example
Here is the complete, runnable implementation of a Vending Machine with transitions across NoCoin, HasCoin, and SoldOut.
--- Customer 1: Normal Purchase ---
Payment required. Please insert a coin first.
Coin accepted. Please make a selection.
Selection confirmed.
[Machine] Item dispensed! Remaining stock: 1
--- Customer 2: Insert and Change Mind ---
Coin accepted. Please make a selection.
Coin returned.
--- Customer 3: Purchase Last Item ---
Coin accepted. Please make a selection.
Selection confirmed.
[Machine] Item dispensed! Remaining stock: 0
Variations
State transition tables vs state classes
- Polymorphic State Classes (GoF): State-specific logic and transitions are encapsulated within concrete classes. Adding a new state requires creating a new class without modifying existing classes. However, visualising all transitions across the entire system requires reading through all state files.
- State Transition Tables: A central 2D matrix or map defines transitions:
(CurrentState, Event) -> (NextState, Action). This provides a single declarative dashboard of all legal transitions, making illegal transitions immediately detectable.
Who owns the state transitions?
- State-Driven Transitions: Concrete states know about next states and call
context.setState(new NextState()). This encapsulates state flow within state classes but couples state classes to one another. - Context-Driven Transitions: Concrete states only return status flags or error codes, and the Context determines which state to transition into next. This keeps state classes decoupled from each other but adds transition conditional logic to the context.
Where you've already used it
- TCP Network Sockets: Sockets change behavior based on TCP state (
CLOSED,LISTEN,SYN_SENT,ESTABLISHED). - Spring State Machine: Enterprise state machine framework for modeling order fulfillment, insurance claims, and multi-step payment approvals.
- XState / Frontend Statecharts: Finite state machine library for React and Vue modeling UI modal and form validation workflows.
- Game Engine AI: Non-player characters transitioning between
Idle,Patrol,Alert,Chase, andAttackstates.
Pros and cons
Advantages
- Single Responsibility Principle: Isolates state-dependent logic into dedicated, self-contained classes.
- Open/Closed Principle: Add new states without modifying existing state classes or context methods.
- Eliminates Massive Conditionals: Replaces complex nested
switchstatements with clean polymorphic method calls.
Disadvantages
- Class Overhead: Increases the number of classes substantially for simple state machines with only two states.
- Inter-State Coupling: When states manage transitions, concrete state classes become coupled to other concrete state classes.
When not to use it
Do not use State if an object only has two states and rarely changes behavior (e.g. an active/inactive boolean flag). A simple boolean check is much simpler than creating an interface and two separate state classes.
Related patterns
| Pattern | Behavior Trigger | Class Coupling |
|---|---|---|
| State | Autonomous transitions based on internal state | States frequently know about other states |
| Strategy | Injected by client externally | Strategies are independent and unaware of peers |
| Bridge | Structural separation of concerns | Abstraction and implementation evolve independently |
| Command | Encapsulates single operation | Represents executable actions |
State: order lifecycle machine
Refactor a state machine plagued by conditional switch statements into the State pattern with dedicated Draft, Paid, Shipped, and Cancelled states.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
How does State differ from Strategy? Strategy algorithms are chosen and injected by the client, and strategies are independent and unaware of each other. In State, the context transitions between states automatically as events occur, and state classes frequently know about and instantiate other states.
What is the trade-off between State-driven vs Context-driven transitions? State-driven transitions encapsulate the progression rules cleanly within each state, but introduce coupling between state classes. Context-driven transitions keep states completely decoupled, but clutter the context with conditional transition rules.
Can State objects be shared across multiple context instances? Yes, if the state objects are stateless flyweights (holding no mutable instance fields). If state objects must store local timers, counters, or references to a specific context, each context must instantiate its own state instances.
How does the State pattern satisfy the Open/Closed Principle?
New states can be added to the system by introducing a new State subclass without modifying existing state classes or the context class.
Try it
Vending Machine LLD
Model selection, payment, dispensing and refunds with guarded state transitions.