🔄 State Design Pattern
Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
🌍 Real-World Analogy
Analogy — Vending Machine
A vending machine behaves differently based on its state. When it has no money inserted, pressing the dispense button does nothing. When money is inserted, it shows available items. When an item is selected, it dispenses and returns change. Same buttons, completely different behavior — depending on the machine's current state.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
A["🚫 No Money"] -->|"insert coin"| B["💰 Has Money"] -->|"select item"| C["⚙️ Dispensing"] -->|"item drops"| D["🎉 Take Item"]
D -->|"done"| A
style A fill:#FCE4EC,stroke:#C62828,stroke-width:2px,color:#000
style B fill:#FFF8E1,stroke:#F9A825,stroke-width:2px,color:#000
style C fill:#E3F2FD,stroke:#1565C0,stroke-width:2px,color:#000
style D fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px,color:#000 🏗️ Pattern Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Context["🎮 Context"] -->|"delegates to"| State[["🔄 State"]]
StateA{{"🟢 State A"}} -->|"implements"| State
StateB{{"🟡 State B"}} -->|"implements"| State
StateA -.->|"transitions to"| StateB
style Context fill:#E8F5E9,stroke:#2E7D32,color:#000
style State fill:#FFF3E0,stroke:#E65100,color:#000
style StateA fill:#E3F2FD,stroke:#1565C0,color:#000
style StateB fill:#FFF8E1,stroke:#F9A825,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class OrderState {
<<interface>>
+next(Order order) void
+previous(Order order) void
+cancel(Order order) void
+getStatus() String
}
class Order {
-state: OrderState
-orderId: String
+setState(OrderState state) void
+nextStep() void
+previousStep() void
+cancel() void
+getStatus() String
}
class PendingState {
+next(Order order) void
+previous(Order order) void
+cancel(Order order) void
+getStatus() String
}
class ConfirmedState {
+next(Order order) void
+previous(Order order) void
+cancel(Order order) void
+getStatus() String
}
class ShippedState {
+next(Order order) void
+previous(Order order) void
+cancel(Order order) void
+getStatus() String
}
class DeliveredState {
+next(Order order) void
+previous(Order order) void
+cancel(Order order) void
+getStatus() String
}
class CancelledState {
+next(Order order) void
+previous(Order order) void
+cancel(Order order) void
+getStatus() String
}
Order --> OrderState : delegates to
PendingState ..|> OrderState
ConfirmedState ..|> OrderState
ShippedState ..|> OrderState
DeliveredState ..|> OrderState
CancelledState ..|> OrderState ❓ The Problem
An object's behavior depends on its state, and it must change behavior at runtime:
- You have large conditional blocks (
if/switch) checking the current state before every action - Adding a new state means modifying every method that has state-dependent behavior
- State transitions are scattered throughout the code and hard to track
- The same transitions exist in multiple places — violating DRY
Example: An order processing system where an order goes through PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED — each with different allowed operations.
Without This Pattern
public class Order {
private String status = "PENDING";
public void nextStep() {
if (status.equals("PENDING")) {
status = "CONFIRMED";
} else if (status.equals("CONFIRMED")) {
status = "SHIPPED";
} else if (status.equals("SHIPPED")) {
status = "DELIVERED";
} else if (status.equals("DELIVERED")) {
System.out.println("Already delivered");
}
}
public void cancel() {
if (status.equals("PENDING") || status.equals("CONFIRMED")) {
status = "CANCELLED";
} else if (status.equals("SHIPPED")) {
throw new IllegalStateException("Cannot cancel shipped order");
}
// Every method has these same if/else chains for EVERY state
}
}
- Duplicated conditionals — every method (
nextStep,cancel,refund,modify) repeats the same state-checking if/else ladder - Violates Open/Closed Principle — adding a new state (e.g., "RETURNING") forces modifications to every single method in the class
- Error-prone transitions — it is easy to forget a case in one method, allowing illegal transitions silently
- Unreadable at scale — with 6+ states and 5+ methods, you get 30+ conditional branches in a single class
- Pain point: A developer adds a "PARTIALLY_SHIPPED" state and must hunt through every method to add handling — missing one creates a production bug where orders get stuck
✅ The Solution
The State pattern extracts state-specific behavior into separate state classes:
- Define a State interface with methods for all state-dependent behavior
- Create Concrete State classes, one per state, implementing the behavior for that state
- The Context holds a reference to the current state object and delegates behavior to it
- State transitions happen by replacing the current state object with another
💻 Implementation
// State interface
public interface OrderState {
void next(Order order);
void previous(Order order);
void cancel(Order order);
String getStatus();
}
// Context
public class Order {
private OrderState state;
private final String orderId;
public Order(String orderId) {
this.orderId = orderId;
this.state = new PendingState(); // Initial state
}
public void setState(OrderState state) {
System.out.println("📦 Order " + orderId + ": " +
this.state.getStatus() + " → " + state.getStatus());
this.state = state;
}
public void nextStep() { state.next(this); }
public void previousStep() { state.previous(this); }
public void cancel() { state.cancel(this); }
public String getStatus() { return state.getStatus(); }
}
// Concrete States
public class PendingState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ConfirmedState());
}
@Override
public void previous(Order order) {
System.out.println("⚠️ Already at initial state");
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
}
@Override
public String getStatus() { return "PENDING"; }
}
public class ConfirmedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ShippedState());
}
@Override
public void previous(Order order) {
order.setState(new PendingState());
}
@Override
public void cancel(Order order) {
order.setState(new CancelledState());
}
@Override
public String getStatus() { return "CONFIRMED"; }
}
public class ShippedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new DeliveredState());
}
@Override
public void previous(Order order) {
order.setState(new ConfirmedState());
}
@Override
public void cancel(Order order) {
System.out.println("❌ Cannot cancel — already shipped!");
}
@Override
public String getStatus() { return "SHIPPED"; }
}
public class DeliveredState implements OrderState {
@Override
public void next(Order order) {
System.out.println("✅ Order complete — no further steps");
}
@Override
public void previous(Order order) {
System.out.println("❌ Cannot revert — already delivered");
}
@Override
public void cancel(Order order) {
System.out.println("❌ Cannot cancel — already delivered");
}
@Override
public String getStatus() { return "DELIVERED"; }
}
public class CancelledState implements OrderState {
@Override
public void next(Order order) {
System.out.println("❌ Order cancelled — no transitions allowed");
}
@Override
public void previous(Order order) {
System.out.println("❌ Order cancelled — no transitions allowed");
}
@Override
public void cancel(Order order) {
System.out.println("⚠️ Already cancelled");
}
@Override
public String getStatus() { return "CANCELLED"; }
}
// Usage
public class Main {
public static void main(String[] args) {
Order order = new Order("ORD-001");
order.nextStep(); // PENDING → CONFIRMED
order.nextStep(); // CONFIRMED → SHIPPED
order.cancel(); // ❌ Cannot cancel — already shipped!
order.nextStep(); // SHIPPED → DELIVERED
order.nextStep(); // ✅ Order complete — no further steps
}
}
// State interface
public interface PlayerState {
void play(MediaPlayer player);
void pause(MediaPlayer player);
void stop(MediaPlayer player);
}
public class MediaPlayer {
private PlayerState state;
private String currentTrack;
public MediaPlayer() {
this.state = new StoppedState();
}
public void setState(PlayerState state) { this.state = state; }
public void setCurrentTrack(String track) { this.currentTrack = track; }
public String getCurrentTrack() { return currentTrack; }
public void play() { state.play(this); }
public void pause() { state.pause(this); }
public void stop() { state.stop(this); }
}
public class StoppedState implements PlayerState {
@Override
public void play(MediaPlayer player) {
System.out.println("▶️ Playing: " + player.getCurrentTrack());
player.setState(new PlayingState());
}
@Override
public void pause(MediaPlayer player) {
System.out.println("⚠️ Can't pause — nothing playing");
}
@Override
public void stop(MediaPlayer player) {
System.out.println("⚠️ Already stopped");
}
}
public class PlayingState implements PlayerState {
@Override
public void play(MediaPlayer player) {
System.out.println("⚠️ Already playing");
}
@Override
public void pause(MediaPlayer player) {
System.out.println("⏸️ Paused");
player.setState(new PausedState());
}
@Override
public void stop(MediaPlayer player) {
System.out.println("⏹️ Stopped");
player.setState(new StoppedState());
}
}
public class PausedState implements PlayerState {
@Override
public void play(MediaPlayer player) {
System.out.println("▶️ Resuming: " + player.getCurrentTrack());
player.setState(new PlayingState());
}
@Override
public void pause(MediaPlayer player) {
System.out.println("⚠️ Already paused");
}
@Override
public void stop(MediaPlayer player) {
System.out.println("⏹️ Stopped");
player.setState(new StoppedState());
}
}
🎯 When to Use
- When an object's behavior depends on its state and must change at runtime
- When you have large conditionals that switch behavior based on the object's state
- When state transitions have complex rules and you want them explicit
- When you want to model a finite state machine in an object-oriented way
- When state-specific behavior should be extensible (new states without modifying existing ones)
🏭 Real-World Examples
| Framework/Library | Usage |
|---|---|
Java Thread.State | NEW, RUNNABLE, BLOCKED, WAITING, TERMINATED |
| Spring State Machine | spring-statemachine — full state machine framework |
| JSF Lifecycle | Request processing phases as states |
| TCP Connection | LISTEN, ESTABLISHED, CLOSE_WAIT, etc. |
| Workflow Engines (Camunda, Activiti) | Task states in BPMN processes |
| Iterator | Internal state tracks position (has next, exhausted) |
⚠️ Pitfalls
Common Mistakes
- State explosion — Too many states with too many transitions becomes unmanageable. Consider a state machine library for complex cases.
- Hardcoded transitions — State classes directly instantiate the next state. Use a transition table for flexibility.
- Shared state objects — If state objects are shared (flyweight), ensure they are stateless. Otherwise, create new instances.
- Context coupling — State objects need a reference to the context for transitions, creating a circular dependency. Keep the interface minimal.
- Forgetting terminal states — Always define behavior for terminal states (delivered, cancelled) to prevent illegal transitions.
📝 Key Takeaways
Summary
- State eliminates complex conditionals by delegating behavior to state objects
- Each state class handles only its own behavior — Single Responsibility Principle
- New states are added without modifying existing state classes — Open/Closed Principle
- State is similar to Strategy, but states know about each other and trigger transitions
- For complex state machines, prefer dedicated libraries (Spring State Machine) over hand-rolling
- The pattern makes state transitions explicit and traceable — invaluable for debugging