State Design Pattern in Java

The State pattern lets a delivery respond differently as it moves from packed to shipped to delivered. Follow the Java example, diagrams, and invalid transition.

State design pattern

The State pattern lets one object respond differently to the same request as its current state changes. The object keeps a reference to a state object and passes the request to it. Replacing that state object changes what the next request does, while the original object’s Java class stays the same.

“Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.”

Consider a shipping application that records a package’s progress from the warehouse to the customer. When the package is packed, an advance request marks it as shipped. The next advance marks it as delivered. Once delivered, another advance is invalid. We will put those stage-specific rules in separate state classes so the delivery can handle the same request at each stage.

1. How the State Pattern Works

The pattern separates the object the caller uses from the objects that supply its current behavior. It has four participants.

Participant What it does
Context The context holds the current state and delegates a request to it.
State interface The interface declares the operations that the context can request from any state.
Concrete states Each concrete state implements those operations for one stage. A state may also cause the context to move to another state.
Client The client calls the context without needing to choose a concrete state for each request.

The client calls an operation on the context. The context forwards it to the current state. That state performs the work, rejects the request or replaces itself with the next state. The context keeps the same public operation while its response can change. The diagram shows the class relationships, not the order of lifecycle stages.

UML class diagram showing a client calling a context that holds a state interface implemented by concrete state classes

The phrase “appear to change its class” describes the change that a caller observes. The context object does not turn into an instance of another Java class. Its state reference points to a different implementation, and that implementation supplies the behavior of the next request.

2. State Pattern in Action

Our example is a delivery record in a shipping application. The caller asks the delivery to advance() when the next stage of shipping has been completed. A packed package can become shipped, and a shipped package can become delivered. Delivered is a terminal state, meaning this simple process has no next stage. An advance request at that point must fail rather than silently starting the process again.

Allowed transitions from Packed to Shipped to Delivered, where Delivered has no next stage

The classes in our example fill the pattern roles as follows.

Pattern participant Class or interface What it does
Context Delivery The delivery holds its current PackageState and sends advance() to that state.
State interface PackageState The interface declares advance(Delivery) and name() for every delivery stage.
Concrete state Packed This state changes the delivery to Shipped when it receives advance().
Concrete state Shipped This state changes the delivery to Delivered when it receives advance().
Concrete state Delivered This state rejects advance() because the delivery has reached its final stage.
Client StateDemo The demo creates a delivery, calls advance() and prints the result of each request.

The Delivery object starts with Packed. On the first request, Delivery.advance() calls Packed.advance(), which replaces the current state with Shipped. The next request follows the same route through Delivery, but now Shipped.advance() runs. A third request reaches Delivered.advance() and throws an exception. The caller uses one method throughout; the active state determines what that method does.

UML class diagram showing StateDemo calling Delivery, Delivery holding PackageState, and Packed, Shipped and Delivered implementing it

3. State Pattern Implementation

We use separate source files for the complete example. The excerpts below omit package declarations, imports, comments and some repeated methods so we can focus on the behavior. The complete source on GitHub includes all files needed to compile and run it.

3.1. Define the State Interface and Context

PackageState declares advance(Delivery), which receives the delivery that may change stage. It also declares name() so the delivery can report its current stage.

interface PackageState {
    void advance(Delivery delivery);
    String name();
}

Delivery holds one PackageState reference and initializes it to Packed. Its public advance() method passes the request to that reference. The changeState() method is package-private, so only classes in the same Java package can call it. It gives a state implementation a controlled way to set the next stage and rejects a null replacement.

public final class Delivery {
    private PackageState state = new Packed();

    public void advance() {
        state.advance(this);
    }

    void changeState(PackageState next) {
        state = Objects.requireNonNull(next);
    }

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

The delivery stays the same object. Only its state reference changes. status() asks the current state for a readable name so the demo can show each transition.

3.2. Implement Valid and Invalid Transitions

Packed advances the delivery to Shipped. Shipped uses the same method to move it to Delivered. The two implementations make the allowed forward path explicit.

final class Packed implements PackageState {
    @Override
    public void advance(Delivery delivery) {
        delivery.changeState(new Shipped());
    }

    @Override
    public String name() {
        return "Packed";
    }
}

final class Shipped implements PackageState {
    @Override
    public void advance(Delivery delivery) {
        delivery.changeState(new Delivered());
    }

    @Override
    public String name() {
        return "Shipped";
    }
}

Delivered has no successor. It throws IllegalStateException when advance() is called, which matches a request made when the delivery is in the wrong state for that operation. The exception leaves the delivery in Delivered.

final class Delivered implements PackageState {
    @Override
    public void advance(Delivery delivery) {
        throw new IllegalStateException("The package is already delivered");
    }

    @Override
    public String name() {
        return "Delivered";
    }
}

The Java documentation for IllegalStateException describes it as a signal that a method was called at an inappropriate time. A larger shipping system might add cancellation, failed delivery or a return path. Those transitions need their own rules; State does not make every stage reachable from every other stage.

4. Demo

StateDemo creates a new delivery and prints its status before and after two valid advance requests. It then makes one more request to demonstrate how the final state responds.

public final class StateDemo {
    public static void main(String[] args) {
        Delivery delivery = new Delivery();
        System.out.println(delivery.status());
        delivery.advance();
        System.out.println(delivery.status());
        delivery.advance();
        System.out.println(delivery.status());

        try {
            delivery.advance();
        } catch (IllegalStateException exception) {
            System.out.println(exception.getMessage());
        }
        System.out.println("Status after rejected advance = " + delivery.status());
    }
}

After cloning the GitHub repository, open a terminal in its root folder. These commands compile the source and run the demo.

javac -d target/state-demo -sourcepath src/main/java src/main/java/com/howtodoinjava/patterns/behavioral/state/StateDemo.java
java -cp target/state-demo com.howtodoinjava.patterns.behavioral.state.StateDemo
Packed
Shipped
Delivered
The package is already delivered
Status after rejected advance = Delivered

The first three lines show the delivery moving through its two allowed transitions. The fourth line is the message from the rejected request after delivery. The final line confirms that the delivery remains in Delivered. The failed request does not move the delivery out of its terminal state. This example changes an in-memory object; it does not contact a carrier or save a delivery record.

5. Similar Patterns

State and Strategy both let a context delegate work to an object behind an interface. The reason for choosing that object differs.

Pattern What changes When to choose it
State The context’s current stage determines its response and may determine the next stage. Choose State when several operations have distinct behavior across stages of one object’s life.
Strategy A caller or configuration supplies an interchangeable way to perform a task. Choose Strategy when the problem is selecting an algorithm or rule, such as a discount calculation.
Template Method A base class fixes a sequence while subclasses implement selected steps. Choose Template Method when the sequence is shared and subclass steps vary, rather than having a context delegate to a current state object.

A short switch or a few conditions may still be clearer for a small, fixed set of rules. State adds classes and transition relationships to maintain. It becomes useful when stage-specific behavior is substantial or repeated across multiple operations, and the conditions are making those operations hard to follow. Adding a new stage may still require changes to the transitions that lead into or out of it.

6. FAQs

6.1. Does the Delivery Object Really Change Its Java Class?

No. Delivery remains a Delivery throughout the example. Its state field first points to Packed, then Shipped, then Delivered. Because those classes implement the same interface differently, calls to Delivery.advance() produce different behavior. That is what the original definition means by “appear to change its class.”

6.2. Who Decides When the State Changes?

The pattern does not require one location for transition rules. In this example, Packed and Shipped choose their successors by calling Delivery.changeState(). Another application may keep transition policy in the context or a separate component if that makes its rules easier to manage. The client should still use the context’s public operations rather than silently replacing its state behind those rules.

6.3. Can State Objects Be Shared?

A state object with no data specific to one delivery can be shared if its implementation is safe to reuse. Our example creates a new state object at each transition because it keeps the flow simple. If a state stores mutable data for a particular delivery, sharing it across deliveries could mix their data. Sharing state objects also does not make the mutable Delivery context safe for concurrent use.

7. Conclusion

State puts stage-specific behavior beside the rules for each stage. The delivery exposes one advance() operation, while Packed, Shipped and Delivered decide what that request does. The pattern is useful when an object’s lifecycle has enough varying behavior to justify explicit state classes; for a short, fixed rule, a condition may be simpler.

Happy Learning!

Leave a Comment

  1. Hi Lokesh, thank you for all the contribution of yours towards this road-map of the java tech world.

    Please correct the delivered state class, space is missing between Delivered and instance on both the methods and singleton object.

    Though I am still struggling to get head around the technology, If somehow I become successful in the near future…you will be a major part of my success.

    :)

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.