StackWise
Design PatternsBehavioral

State Pattern

Let an object change its behaviour when its internal state changes — replacing sprawling conditionals with state objects.

intermediate 3 min readLesson 22 of 25

Intent

State models an object as a finite state machine. Each state is its own class implementing a common interface; the object (the context) delegates behaviour to its current state object and swaps it when a transition occurs. To the outside world, the object appears to change its class.

The problem

void ship() {
    if (status == NEW) throw new IllegalStateException("Not paid");
    else if (status == PAID) { status = SHIPPED; notifyCourier(); }
    else if (status == SHIPPED) throw new IllegalStateException("Already shipped");
    else if (status == CANCELLED) throw new IllegalStateException("Cancelled");
}
// …and the same if/else ladder in pay(), cancel(), deliver(), refund()…

Every operation repeats a ladder of if (status == ...). Adding a state means editing every method.

The state machine

stateDiagram-v2
  [*] --> New
  New --> Paid : pay
  New --> Cancelled : cancel
  Paid --> Shipped : ship
  Paid --> Cancelled : cancel / refund
  Shipped --> Delivered : deliver
  Delivered --> [*]
  Cancelled --> [*]

Example: an order lifecycle

StateDemo.javaJava 21
import java.util.*;

sealed interface OrderState permits New, Paid, Shipped, Delivered, Cancelled {
    default OrderState pay(Order o)     { throw illegal("pay"); }
    default OrderState ship(Order o)    { throw illegal("ship"); }
    default OrderState deliver(Order o) { throw illegal("deliver"); }
    default OrderState cancel(Order o)  { throw illegal("cancel"); }
    private IllegalStateException illegal(String action) {
        return new IllegalStateException("Cannot " + action + " an order that is " + getClass().getSimpleName());
    }
}

record New() implements OrderState {
    public OrderState pay(Order o)    { o.log("payment captured"); return new Paid(); }
    public OrderState cancel(Order o) { o.log("cancelled before payment"); return new Cancelled(); }
}
record Paid() implements OrderState {
    public OrderState ship(Order o)   { o.log("handed to courier"); return new Shipped(); }
    public OrderState cancel(Order o) { o.log("refund issued"); return new Cancelled(); }
}
record Shipped() implements OrderState {
    public OrderState deliver(Order o) { o.log("delivered"); return new Delivered(); }
}
record Delivered() implements OrderState {}
record Cancelled() implements OrderState {}

class Order {                                       // the context
    private final String id;
    private OrderState state = new New();
    Order(String id) { this.id = id; }

    void pay()     { state = state.pay(this); }
    void ship()    { state = state.ship(this); }
    void deliver() { state = state.deliver(this); }
    void cancel()  { state = state.cancel(this); }

    void log(String msg) { System.out.println("  " + id + ": " + msg); }
    String status() { return state.getClass().getSimpleName(); }
}

class Main {
    static void attempt(Order o, Runnable action) {
        try { action.run(); } catch (IllegalStateException e) { System.out.println("  ✗ " + e.getMessage()); }
    }

    public static void main(String[] args) {
        var a = new Order("A-1");
        a.pay(); a.ship(); a.deliver();
        System.out.println("A-1 is " + a.status());

        var b = new Order("B-2");
        attempt(b, b::ship);                        // not paid yet
        b.pay(); b.cancel();
        attempt(b, b::ship);                        // cancelled
        System.out.println("B-2 is " + b.status());
    }
}

Each state class contains only the transitions valid from that state. Illegal transitions fall through to the default methods. Adding a Returned state means one new record and a change to Delivered — nothing else.

Where you’ll see it

  • Spring State Machine — declarative state machines with guards, actions and persistence
  • java.util.Iterator implementations and parsers often track internal states
  • TCP connection handling (LISTEN, ESTABLISHED, CLOSE_WAIT…), the original GoF example
  • Workflow engines (Camunda, Temporal), order/payment/booking lifecycles, UI wizards, game AI

State vs Strategy

They have the same UML — a context holding an interface. The difference:

State Strategy
Who chooses the object? The states themselves trigger transitions The client picks a strategy
Do implementations know each other? Yes — a state returns the next state No — strategies are independent
Changes over the object’s life? Constantly Usually fixed once configured
Finished this lesson?

Mark it complete to track your progress through Design Patterns.