You’re building an order processing system. Orders can be Pending, Paid, Shipped, Delivered, or Cancelled. Each state has different rules: you can cancel a Pending order, but not a Shipped one. You can ship a Paid order, but not a Pending one.

You could write a giant switch statement. Or you could let each state handle its own behavior.

What is the State Pattern?

State allows an object to alter its behavior when its internal state changes. The object appears to change its class. Each state is a separate class that implements state-specific behavior.

State Design Pattern class diagram showing Order context with State interface and Pending, Paid, Shipped concrete states for behavior changes

Order holds a reference to the current State (Pending, Paid, or Shipped). When you call next(), it delegates to the current State object. States can transition the Order to the next state.

When to Use State

Use State When Skip State When
Object behavior varies significantly by state Behavior is the same regardless of state
State transitions are complex States are simple or few
You have large conditional blocks checking state Conditions are simple
States should be extensible All states are known and fixed

State replaces complex conditionals with polymorphism. Each state class handles its own logic.

Implementation

Order Processing System

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public interface OrderState {
    void pay(OrderContext order);
    void ship(OrderContext order);
    void deliver(OrderContext order);
    void cancel(OrderContext order);
    String getStatus();
}

public class OrderContext {
    private OrderState state;
    private final String orderId;
    private final List<String> history = new ArrayList<>();
    
    public OrderContext(String orderId) {
        this.orderId = orderId;
        this.state = new PendingState();
        logTransition("Order created");
    }
    
    public void setState(OrderState state) {
        this.state = state;
        logTransition("State changed to: " + state.getStatus());
    }
    
    public void pay() {
        state.pay(this);
    }
    
    public void ship() {
        state.ship(this);
    }
    
    public void deliver() {
        state.deliver(this);
    }
    
    public void cancel() {
        state.cancel(this);
    }
    
    public String getStatus() {
        return state.getStatus();
    }
    
    private void logTransition(String message) {
        history.add(LocalDateTime.now() + ": " + message);
    }
    
    public List<String> getHistory() {
        return Collections.unmodifiableList(history);
    }
}

State Implementations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
public class PendingState implements OrderState {
    
    @Override
    public void pay(OrderContext order) {
        System.out.println("Payment received. Order is now paid.");
        order.setState(new PaidState());
    }
    
    @Override
    public void ship(OrderContext order) {
        System.out.println("Cannot ship. Order is not paid yet.");
    }
    
    @Override
    public void deliver(OrderContext order) {
        System.out.println("Cannot deliver. Order is not shipped yet.");
    }
    
    @Override
    public void cancel(OrderContext order) {
        System.out.println("Order cancelled.");
        order.setState(new CancelledState());
    }
    
    @Override
    public String getStatus() {
        return "PENDING";
    }
}

public class PaidState implements OrderState {
    
    @Override
    public void pay(OrderContext order) {
        System.out.println("Order is already paid.");
    }
    
    @Override
    public void ship(OrderContext order) {
        System.out.println("Order shipped.");
        order.setState(new ShippedState());
    }
    
    @Override
    public void deliver(OrderContext order) {
        System.out.println("Cannot deliver. Order is not shipped yet.");
    }
    
    @Override
    public void cancel(OrderContext order) {
        System.out.println("Refund initiated. Order cancelled.");
        order.setState(new CancelledState());
    }
    
    @Override
    public String getStatus() {
        return "PAID";
    }
}

public class ShippedState implements OrderState {
    
    @Override
    public void pay(OrderContext order) {
        System.out.println("Order is already paid.");
    }
    
    @Override
    public void ship(OrderContext order) {
        System.out.println("Order is already shipped.");
    }
    
    @Override
    public void deliver(OrderContext order) {
        System.out.println("Order delivered successfully.");
        order.setState(new DeliveredState());
    }
    
    @Override
    public void cancel(OrderContext order) {
        System.out.println("Cannot cancel. Order is already shipped.");
    }
    
    @Override
    public String getStatus() {
        return "SHIPPED";
    }
}

public class DeliveredState implements OrderState {
    
    @Override
    public void pay(OrderContext order) {
        System.out.println("Order is already paid and delivered.");
    }
    
    @Override
    public void ship(OrderContext order) {
        System.out.println("Order is already delivered.");
    }
    
    @Override
    public void deliver(OrderContext order) {
        System.out.println("Order is already delivered.");
    }
    
    @Override
    public void cancel(OrderContext order) {
        System.out.println("Cannot cancel. Order is already delivered. Please initiate return.");
    }
    
    @Override
    public String getStatus() {
        return "DELIVERED";
    }
}

public class CancelledState implements OrderState {
    
    @Override
    public void pay(OrderContext order) {
        System.out.println("Cannot pay. Order is cancelled.");
    }
    
    @Override
    public void ship(OrderContext order) {
        System.out.println("Cannot ship. Order is cancelled.");
    }
    
    @Override
    public void deliver(OrderContext order) {
        System.out.println("Cannot deliver. Order is cancelled.");
    }
    
    @Override
    public void cancel(OrderContext order) {
        System.out.println("Order is already cancelled.");
    }
    
    @Override
    public String getStatus() {
        return "CANCELLED";
    }
}

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
OrderContext order = new OrderContext("ORD-12345");

System.out.println("Status: " + order.getStatus());  // PENDING

order.ship();    // Cannot ship. Order is not paid yet.
order.pay();     // Payment received. Order is now paid.
System.out.println("Status: " + order.getStatus());  // PAID

order.ship();    // Order shipped.
System.out.println("Status: " + order.getStatus());  // SHIPPED

order.cancel();  // Cannot cancel. Order is already shipped.
order.deliver(); // Order delivered successfully.
System.out.println("Status: " + order.getStatus());  // DELIVERED

State Diagram

stateDiagram-v2
    [*] --> Pending
    Pending --> Paid : pay()
    Pending --> Cancelled : cancel()
    Paid --> Shipped : ship()
    Paid --> Cancelled : cancel()
    Shipped --> Delivered : deliver()
    Delivered --> [*]
    Cancelled --> [*]

State Machine with Guards

Add conditions to transitions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public interface DocumentState {
    void submit(DocumentContext doc);
    void approve(DocumentContext doc, User approver);
    void reject(DocumentContext doc, String reason);
    void publish(DocumentContext doc);
    String getStatus();
}

public class DraftState implements DocumentState {
    
    @Override
    public void submit(DocumentContext doc) {
        if (doc.getContent().isEmpty()) {
            System.out.println("Cannot submit empty document");
            return;
        }
        System.out.println("Document submitted for review");
        doc.setState(new PendingReviewState());
    }
    
    @Override
    public void approve(DocumentContext doc, User approver) {
        System.out.println("Cannot approve. Document not submitted yet.");
    }
    
    @Override
    public void reject(DocumentContext doc, String reason) {
        System.out.println("Cannot reject. Document not submitted yet.");
    }
    
    @Override
    public void publish(DocumentContext doc) {
        System.out.println("Cannot publish. Document not approved yet.");
    }
    
    @Override
    public String getStatus() {
        return "DRAFT";
    }
}

public class PendingReviewState implements DocumentState {
    
    @Override
    public void submit(DocumentContext doc) {
        System.out.println("Document already submitted for review.");
    }
    
    @Override
    public void approve(DocumentContext doc, User approver) {
        if (!approver.hasRole("REVIEWER")) {
            System.out.println("User is not authorized to approve.");
            return;
        }
        
        doc.setApprover(approver);
        System.out.println("Document approved by " + approver.getName());
        doc.setState(new ApprovedState());
    }
    
    @Override
    public void reject(DocumentContext doc, String reason) {
        doc.setRejectionReason(reason);
        System.out.println("Document rejected: " + reason);
        doc.setState(new DraftState());  // Back to draft for revision
    }
    
    @Override
    public void publish(DocumentContext doc) {
        System.out.println("Cannot publish. Document not approved yet.");
    }
    
    @Override
    public String getStatus() {
        return "PENDING_REVIEW";
    }
}

State vs Strategy

This is a common confusion. Here’s the difference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// STRATEGY - client picks the algorithm
public class PaymentService {
    private PaymentStrategy strategy;
    
    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;  // Client decides
    }
    
    public void pay(Money amount) {
        strategy.pay(amount);
    }
}

// Client controls which strategy to use
paymentService.setStrategy(new CreditCardStrategy());
paymentService.pay(money);

// STATE - object changes its own behavior
public class TrafficLight {
    private TrafficLightState state;
    
    public TrafficLight() {
        this.state = new RedState();  // Initial state
    }
    
    public void next() {
        state.next(this);  // State decides what comes next
    }
}

// State transitions are internal
trafficLight.next();  // Red -> Green
trafficLight.next();  // Green -> Yellow
trafficLight.next();  // Yellow -> Red

With Strategy, the client picks. With State, the object transitions itself.

Shared State Objects

If states are stateless, you can share instances:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TrafficLightStates {
    public static final TrafficLightState RED = new RedState();
    public static final TrafficLightState YELLOW = new YellowState();
    public static final TrafficLightState GREEN = new GreenState();
}

public class RedState implements TrafficLightState {
    @Override
    public void next(TrafficLight light) {
        light.setState(TrafficLightStates.GREEN);
    }
    
    @Override
    public String getColor() {
        return "RED";
    }
}

This works when states don’t hold context-specific data.

Common Mistakes

1. Giant State Interface

Don’t force all states to implement irrelevant methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Bad - every state must implement everything
public interface State {
    void methodForStateA();
    void methodForStateB();
    void methodForStateC();
    // States end up with many no-op implementations
}

// Better - use abstract base class with default behavior
public abstract class AbstractState implements State {
    @Override
    public void methodForStateA(Context ctx) {
        throw new IllegalStateException("Operation not allowed in " + getStatus());
    }
    // Other methods with sensible defaults
}

2. States Knowing Too Much

States should know about transitions, not business logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Bad - business logic in state
public class PaidState implements OrderState {
    @Override
    public void ship(OrderContext order) {
        if (inventory.check(order.getItems())) {  // Wrong place
            warehouse.reserve(order.getItems());   // Wrong place
            shipping.schedule(order);              // Wrong place
            order.setState(new ShippedState());
        }
    }
}

// Better - state handles transition, context handles business logic
public class PaidState implements OrderState {
    @Override
    public void ship(OrderContext order) {
        if (order.canShip()) {  // Context knows the rules
            order.processShipment();  // Context handles business logic
            order.setState(new ShippedState());
        }
    }
}

3. Forgetting Terminal States

Terminal states should be explicit:

1
2
3
4
5
6
7
public class DeliveredState implements OrderState {
    @Override
    public void pay(OrderContext order) {
        // Don't silently ignore
        throw new IllegalStateException("Order is complete - no further actions");
    }
}

Real-World Examples

TCP Connection: States like LISTEN, SYN_SENT, ESTABLISHED, CLOSE_WAIT.

Media Player: States like Playing, Paused, Stopped.

Vending Machine: States like Idle, HasMoney, Dispensing.

Game Character: States like Idle, Walking, Running, Jumping, Attacking.

Strategy is similar but externally controlled. State transitions internally.

Flyweight can share stateless State objects.

Singleton is often used for shared state instances.

State machines often use State pattern internally.

Wrapping Up

State pattern lets objects change behavior based on internal state. Each state is a class with its own implementation of state-specific behavior.

Use State when behavior varies significantly by state and transitions are complex. It replaces sprawling switch statements with clean, polymorphic code.

Remember: State controls its own transitions. The client doesn’t pick states, the object moves through them based on operations and rules.


Further Reading: