00:00

State Design Pattern in Java

Last updated:

The state design pattern in Java lets an object change its behaviour when its internal state changes, by moving each state's behaviour into its own class. The object, called the context, delegates every action to its current state object, and the state object decides what the action does and which state comes next.

It matters because objects with a lifecycle (orders, payments, loan applications, connections) otherwise fill up with if and switch checks on a status field. Every method repeats the same checks, and one wrong condition lets a shipped order be cancelled. The State pattern gives each state one place for its rules. It is a behavioral design pattern, and it is how state machines are usually implemented in object-oriented code.

1. What Is the State Design Pattern?

The GoF intent is: "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class."

Instead of a status field checked everywhere, the context holds a reference to a state object. Every state implements the same interface, which has one method per action the context supports. When the client calls order.ship(), the order simply calls state.ship(this). A PaidState ships the order and switches the context to ShippedState; a NewState refuses.

From the outside, the same Order object behaves differently depending on its state, as if its class had changed. Inside, each state class contains only the rules for that state, which follows the Single Responsibility Principle. Adding a state means adding a class and updating the states that lead to it, rather than editing every method of the context.

2. Key Components

ComponentResponsibilityIn the Basic Example
ContextHolds the current state and delegates actions to itOrder
StateInterface with one method per actionOrderState
Concrete StatesImplement the allowed actions and choose the next stateNewState, PaidState, ShippedState, DeliveredState, CancelledState
ClientCalls actions on the context, never on statesmain

The client calls the context. The context forwards the call, passing itself, to the current state. The state does its work and, if the action causes a transition, tells the context which state to switch to.

3. When Is State Useful?

  • Order lifecycles: new, paid, shipped, delivered and cancelled orders accept different actions.
  • Resilience: a circuit breaker behaves completely differently when closed, open or half-open.
  • Payments: authorise, capture and refund are valid only in specific states, and gateway webhooks can arrive twice.

4. UML Class Diagram

The diagram shows the Basic Example.

State UML class diagram Order holds one current OrderState and delegates actions to it. NewState, PaidState, ShippedState, DeliveredState and CancelledState implement OrderState, and states call changeState on the Order. context Order - state: OrderState + pay(), ship() + deliver(), cancel() ~ changeState(next) + status(): String current state 1 «interface» OrderState + pay(order), ship(order) + deliver(order), cancel(order) + name(): String implements NewState PaidState ShippedState DeliveredState CancelledState changeState()
State: Order keeps one current OrderState and delegates to it; the states decide transitions by calling back changeState().
  1. Order holds exactly one current state (solid arrow, multiplicity 1).
  2. Five concrete states implement OrderState (dashed lines, hollow triangle).
  3. Each state method receives the order, and states call changeState() to move it on (dashed dependency on the left).
  4. changeState() is package-private (~), so clients cannot jump the order into an arbitrary state.

5. Step-by-Step Execution Flow

  1. A new Order starts with NewState.
  2. The client calls order.pay(); the order calls state.pay(this).
  3. NewState.pay() allows it and calls order.changeState(new PaidState()).
  4. The client calls order.ship(); PaidState moves the order to ShippedState.
  5. The client calls order.cancel(). ShippedState does not override cancel(), so the interface's default throws "cannot cancel an order that is SHIPPED".
  6. The order stays shipped; the client reports the rejection.

6. Real-World Use Cases

  • E-commerce: order, return and shipment lifecycles where each status allows different actions.
  • Payments and banking: payment states (created, authorised, captured, refunded), loan applications and card lifecycles (active, blocked, expired).
  • Notifications: delivery tracking such as queued, sent, delivered, bounced.
  • Resilience libraries: Resilience4j's circuit breaker has CLOSED, OPEN and HALF_OPEN states with different behaviour in each.
  • Spring: the Spring Statemachine project provides a framework for declaring states, events and transitions. In the JDK, Thread.State names a thread's lifecycle states, though as an enum, not as behaviour-carrying state objects.

7. Problem Without the Pattern

An order with a status enum and checks in every method:

OrderWithoutState.java
public class OrderWithoutState {

    enum Status { NEW, PAID, SHIPPED, DELIVERED, CANCELLED }

    // Every method re-checks the status with its own if/switch
    static final class Order {
        private Status status = Status.NEW;

        void pay() {
            if (status != Status.NEW) throw new IllegalStateException("cannot pay when " + status);
            status = Status.PAID;
        }

        void ship() {
            if (status != Status.PAID) throw new IllegalStateException("cannot ship when " + status);
            status = Status.SHIPPED;
        }

        void deliver() {
            if (status != Status.SHIPPED) throw new IllegalStateException("cannot deliver when " + status);
            status = Status.DELIVERED;
        }

        void cancel() {
            if (status == Status.DELIVERED) throw new IllegalStateException("cannot cancel when " + status);
            status = Status.CANCELLED;          // bug: SHIPPED orders can also be cancelled
        }
    }

    public static void main(String[] args) {
        Order order = new Order();
        order.pay();
        order.ship();
        order.cancel();                         // the parcel is already with the courier
        System.out.println("Status: " + order.status + " (a shipped order was cancelled)");
    }
}
Console
Status: CANCELLED (a shipped order was cancelled)
  • Scattered rules: the transition rules for one status are spread across every method.
  • Easy mistakes: cancel() lists the forbidden states instead of the allowed ones, so a shipped order is cancelled.
  • Growth problem: a new status, such as RETURNED, means revisiting every method.
  • State-specific work like issuing a refund ends up as more conditions inside these methods.

8. Solution Using the Pattern

Give each status its own class implementing an OrderState interface. The interface's default methods reject every action, and each state overrides only the actions it allows. The order delegates to its current state.

Allowed transitions are now opt-in: a shipped order cannot be cancelled because ShippedState never says it can. State-specific work, such as the refund when a paid order is cancelled, lives in the state where it belongs. A RETURNED state is one new class plus a returnItem() override in DeliveredState.

9. State Design Pattern in Java: Code Examples

Each example is a single file that runs on Java 21 with java FileName.java.

Basic Example: Order Lifecycle

Problem: fix section 7 so that each order status allows only its own actions.

OrderStateDemo.java
public class OrderStateDemo {

    // State: every action an order supports; by default an action is not allowed
    interface OrderState {
        String name();

        default void pay(Order order) { throw notAllowed("pay"); }
        default void ship(Order order) { throw notAllowed("ship"); }
        default void deliver(Order order) { throw notAllowed("deliver"); }
        default void cancel(Order order) { throw notAllowed("cancel"); }

        private IllegalStateException notAllowed(String action) {
            return new IllegalStateException("cannot " + action + " an order that is " + name());
        }
    }

    // Concrete states: each one overrides only the actions it allows
    static final class NewState implements OrderState {
        public String name() { return "NEW"; }
        public void pay(Order order) { order.changeState(new PaidState()); }
        public void cancel(Order order) { order.changeState(new CancelledState()); }
    }

    static final class PaidState implements OrderState {
        public String name() { return "PAID"; }
        public void ship(Order order) { order.changeState(new ShippedState()); }
        public void cancel(Order order) {
            System.out.println("  refund issued for " + order.id());
            order.changeState(new CancelledState());
        }
    }

    static final class ShippedState implements OrderState {
        public String name() { return "SHIPPED"; }
        public void deliver(Order order) { order.changeState(new DeliveredState()); }
    }

    static final class DeliveredState implements OrderState {
        public String name() { return "DELIVERED"; }
    }

    static final class CancelledState implements OrderState {
        public String name() { return "CANCELLED"; }
    }

    // Context: delegates every action to its current state
    static final class Order {
        private final String id;
        private OrderState state = new NewState();

        Order(String id) { this.id = id; }

        String id() { return id; }
        String status() { return state.name(); }

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

        void changeState(OrderState next) {               // package-private: only states call it
            System.out.println("  " + id + ": " + state.name() + " -> " + next.name());
            state = next;
        }
    }

    static void attempt(Order order, String action, Runnable step) {
        try {
            step.run();
        } catch (IllegalStateException e) {
            System.out.println("  " + order.id() + ": " + action + " rejected (" + e.getMessage() + ")");
        }
    }

    public static void main(String[] args) {
        Order first = new Order("ORD-11");
        first.pay();
        first.ship();
        attempt(first, "cancel", first::cancel);           // the bug from before is now impossible
        first.deliver();
        System.out.println("ORD-11 final status: " + first.status());

        Order second = new Order("ORD-12");
        second.pay();
        second.cancel();                                    // allowed while PAID, with a refund
        attempt(second, "ship", second::ship);
        System.out.println("ORD-12 final status: " + second.status());
    }
}

Important classes and methods

  • OrderState uses default methods that reject actions, plus a private helper method (Java 9+) to build the message.
  • Each concrete state overrides only what it allows; DeliveredState and CancelledState are terminal and override nothing.
  • Order contains no conditions at all; every action is one line of delegation.

Expected output

Console
  ORD-11: NEW -> PAID
  ORD-11: PAID -> SHIPPED
  ORD-11: cancel rejected (cannot cancel an order that is SHIPPED)
  ORD-11: SHIPPED -> DELIVERED
ORD-11 final status: DELIVERED
  ORD-12: NEW -> PAID
  refund issued for ORD-12
  ORD-12: PAID -> CANCELLED
  ORD-12: ship rejected (cannot ship an order that is CANCELLED)
ORD-12 final status: CANCELLED

How it runs

  1. ORD-11 moves from new to paid to shipped; the cancel attempt is rejected; it is then delivered.
  2. ORD-12 is cancelled while paid, so PaidState issues a refund first.
  3. Shipping a cancelled order is rejected by CancelledState.

10. Best Practices and Common Mistakes

Best practices

  • Reject by default, and let each state opt in to the actions it allows.
  • Make states stateless where possible (enums or shared singletons), and keep data in the context.
  • Keep the transition method non-public, so only states can change state.
  • Thread safety: concurrent events can race. Use a lock or compare-and-set in memory, and optimistic locking or conditional updates in the database.

Common mistakes

  • Letting clients set the state directly, which bypasses the rules.
  • States that know too much, reaching deep into the context instead of calling a few methods.
  • Forgetting persistence: storing only the class name, then failing to rebuild state after a restart.
  • Overengineering: state classes for a two-value flag that a boolean handles clearly.

11. When to Use and When Not to Use

Use it when an object has several states with clearly different behaviour, when transitions have rules that must be enforced, or when status conditions are repeated across many methods.

Avoid it for objects with two or three states and trivial behaviour, where an enum and a small switch are clearer, or when states and transitions are configured by business users at runtime, where a table-driven state machine or a workflow engine fits better.

12. Advantages and Disadvantages

Advantages

  • No scattered conditionals: each state's rules live in one class.
  • Explicit, enforceable transitions.
  • Easy to add states without touching unrelated ones.
  • State-specific work sits next to the rules that trigger it.

Disadvantages

  • More classes, one per state.
  • The full transition map is spread across states, so a diagram helps.
  • Persistence and concurrency need extra design.

13. Comparison with Similar Patterns

AspectStateStrategyCommandEnum with switch
Who changes the behaviourThe states themselves, through transitionsThe client, by injecting a strategyNot applicable: commands are requestsCode that sets the field
Objects know each otherStates know their successorsStrategies are independentCommands know their receiverNot applicable
Typical lifetimeChanges many times per objectUsually fixed per objectOne per requestChanges per object
Good forLifecycles with rich behaviourInterchangeable algorithmsUndo, queues, logsFew states, little behaviour

State and Strategy have the same class structure; the difference is who switches. Choose State when the object moves through a lifecycle on its own, and Strategy when a caller picks an algorithm. Events that drive a state machine are often commands, and an enum with a switch remains the simplest choice for small, behaviour-light lifecycles. The Template Method pattern is unrelated in purpose but often confused because both use subclasses.

14. Summary

  • The state design pattern in Java moves each state's behaviour into its own class, and the context delegates to the current state.
  • Purpose: replace scattered status checks with explicit, per-state rules.
  • Components: Context, State interface, Concrete States, Client.
  • States perform actions and decide transitions by telling the context which state comes next.
  • Used for orders, payments, circuit breakers, loans and delivery tracking.
  • Benefits: no duplicated conditionals, enforceable transitions, easy extension.
  • Reject by default, keep states stateless, and guard transitions against concurrent events.

15. Java Design Pattern Interview Questions and Answers

Q1. State and Strategy have the same UML. How do you explain the difference in an interview?

The structure is identical: a context delegating to an interface. The difference is intent and control. In Strategy, the client chooses the behaviour and it rarely changes; in State, the state objects change the context's state themselves as events happen, so behaviour changes over the object's life. State objects usually know about each other, while strategies do not.

Q2. Should a transition be decided by the state classes or by the context?

Both work. Letting states decide keeps each state's rules together and is the classic approach. Putting a transition table in the context, for example a map from (state, event) to next state, makes the whole lifecycle visible in one place. Larger systems often use a table or a framework such as Spring Statemachine, while states keep their behaviour.

Q3. How do you store and restore a state object in a database?

Store an identifier, typically an enum name in a status column, and map it back to the state object when loading. Enum-based states make this trivial with @Enumerated(EnumType.STRING) in JPA. Avoid storing the ordinal, because adding a constant in the middle would silently change the meaning of existing rows.

@Enumerated(EnumType.STRING)
@Column(name = "status")
private PaymentState state;
Q4. Two service instances process the same payment webhook at the same time. How do you prevent a double transition?

In-memory locks do not help across instances, so the database must decide. Use a conditional update such as UPDATE payments SET status = 'CAPTURED' WHERE id = ? AND status = 'AUTHORIZED' and check the affected row count, or use a JPA @Version column for optimistic locking. The loser sees zero rows updated or an OptimisticLockException and treats the event as a duplicate.

Q5. When would you use a plain enum and switch instead of the State pattern?

When there are only a few states, the behaviour per state is a line or two, and transitions are simple. Java 21's switch expressions over enums are exhaustive, so the compiler flags a missing case. Move to state classes when the switches start appearing in many methods or when states need their own logic and data.