20. State
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class. Each state is encapsulated in a separate class, and the context delegates behavior to the current state object.
Intent and Motivation
Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
Motivation: A TCP connection behaves differently when Established, Listening, or Closed — the same connect() call does nothing when closed but initiates handshake when listening. A giant switch(state) in the Connection class is unmaintainable. State pattern gives each state its own class (EstablishedState, ClosedState) implementing a common ConnectionState interface. The Connection context holds a reference to the current state and delegates all state-dependent methods to it. State transitions happen inside state classes (e.g., ClosedState.connect() switches context to ListeningState).
This models finite state machines cleanly and is used in workflows, UI wizards, and game AI.
Structure (UML-like)
┌──────────────┐ delegates ┌──────────────┐
│ Context │ ───────────► │ State │ (interface)
├──────────────┤ ├──────────────┤
│ - state │ │ + handle() │
│ + request() │ │ + enter() │
│ + setState() │ └──────▲───────┘
└──────────────┘ │ implements
┌──────┴───────────────────┐
│ │
┌──────┴──────┐ ┌───────┴──────┐
│ ConcreteSt. │ │ ConcreteSt. │
│ A │ │ B │
└─────────────┘ └──────────────┘
Participants:
- Context — maintains a reference to a State; delegates state-specific requests.
- State — interface for state-specific behavior.
- ConcreteState — implements behavior associated with a state; may trigger transitions.
- Client — configures the initial state and triggers requests on the context.
Java Example
// State interface
interface VendingState {
void insertCoin(VendingMachine machine);
void selectProduct(VendingMachine machine);
void dispense(VendingMachine machine);
}
// Concrete States
class IdleState implements VendingState {
public void insertCoin(VendingMachine m) {
System.out.println("Coin inserted");
m.setState(new HasCoinState());
}
public void selectProduct(VendingMachine m) {
System.out.println("Insert coin first");
}
public void dispense(VendingMachine m) {
System.out.println("Insert coin first");
}
}
class HasCoinState implements VendingState {
public void insertCoin(VendingMachine m) {
System.out.println("Coin already inserted");
}
public void selectProduct(VendingMachine m) {
System.out.println("Product selected");
m.setState(new DispensingState());
}
public void dispense(VendingMachine m) {
System.out.println("Select product first");
}
}
class DispensingState implements VendingState {
public void insertCoin(VendingMachine m) { /* wait */ }
public void selectProduct(VendingMachine m) { /* wait */ }
public void dispense(VendingMachine m) {
System.out.println("Dispensing product...");
m.setState(new IdleState());
}
}
// Context
class VendingMachine {
private VendingState state = new IdleState();
void setState(VendingState state) { this.state = state; }
void insertCoin() { state.insertCoin(this); }
void selectProduct() { state.selectProduct(this); }
void dispense() { state.dispense(this); }
}
// Usage
VendingMachine vm = new VendingMachine();
vm.insertCoin();
vm.selectProduct();
vm.dispense();
JavaScript Example
const states = {
draft: {
submit(doc) {
console.log('Submitting for review');
doc.setState('review');
},
publish(doc) { console.log('Cannot publish from draft'); }
},
review: {
submit(doc) { console.log('Already in review'); },
publish(doc) {
console.log('Approved — publishing');
doc.setState('published');
}
},
published: {
submit(doc) { console.log('Already published'); },
publish(doc) { console.log('Already published'); }
}
};
class Document {
constructor() { this.state = 'draft'; }
setState(state) {
this.state = state;
console.log(`State → ${state}`);
}
submit() { states[this.state].submit(this); }
publish() { states[this.state].publish(this); }
}
const doc = new Document();
doc.submit(); // draft → review
doc.publish(); // review → published
Real-World Use Cases
| Framework / System | Usage |
|---|---|
| TCP/IP stack | Connection states: LISTEN, SYN_SENT, ESTABLISHED, CLOSED. |
| Workflow engines (Camunda, Airflow) | Tasks transition through pending → running → completed → failed. |
| React component lifecycle | Mounting, updating, unmounting states change behavior. |
| Media players | Playing, paused, stopped, buffering — each state handles controls differently. |
| Order fulfillment | placed → paid → shipped → delivered — state drives allowed actions. |
| Game AI | Patrol, chase, attack, flee states for NPC behavior. |
Pros and Cons
| Pros | Cons |
|---|---|
| Eliminates large conditional statements — behavior in state classes | Many state classes for complex state machines |
| State-specific behavior is localized and easy to find | State transitions must be carefully managed |
| Open/Closed — add new states without changing context | Context must know about all state classes for transitions |
| State transitions are explicit and traceable | Overkill for objects with only 2–3 simple states |
| Mirrors finite state machine theory directly | State objects may need context reference, creating coupling |
When to Use vs When NOT to Use
Use when:
- An object’s behavior depends on its state, and it must change at runtime.
- Operations have large, multipart conditional statements that depend on state.
- State-specific behavior should be independently extensible.
- You are modeling a finite state machine or workflow.
Do NOT use when:
- The object has only a few states with trivial behavior differences.
- State transitions are rare and a simple enum + switch is readable.
- States do not change at runtime (static behavior — use Strategy or plain inheritance).
- The state machine is so complex that a dedicated FSM library is more appropriate.
Common Mistakes
- Confusing State with Strategy — State transitions are internal and state-controlled; Strategy is chosen externally by the client.
- Context knowing too many state details — transition logic should live in state classes, not the context.
- Missing invalid transition handling — every state method should handle or reject invalid operations.
- Not logging transitions — debugging state machines without transition logs is painful.
- Circular state references — states holding direct references to each other instead of going through context.
Related Patterns
- Strategy — similar structure; Strategy is client-selected, State is self-transitioning.
- Bridge — both use delegation; Bridge separates abstraction from implementation, State varies behavior by state.
- Flyweight — state objects can be flyweights if many contexts share the same state instance.
- Command — state transitions can be triggered by commands; undo can reverse transitions.
- Memento — stores context state for rollback to a previous state.