State Pattern explained simply
If you have ever written a method full of if/else chains or switch statements checking
some status field, you already know the pain. Every time a new state gets added, you are
back in that same method, bolting on another branch and praying you didn’t break the
others. The State Pattern kills that mess entirely.
What is the State Pattern?
- Allows an object to change its behavior when its internal state changes.
- The object will appear to change its class at runtime.
- Encapsulates state-specific behavior into separate classes instead of cramming it all into one big conditional block.
In short:
Different state, different behavior. Same interface.
Real-Life Analogy
Think about a vending machine. It has the same physical buttons no matter what, but what those buttons do depends entirely on the machine’s current state.
- Idle: You press the dispense button. Nothing happens. It is waiting for money.
- Has Money: You press the dispense button. It checks your selection and starts dispensing.
- Dispensing: You press the dispense button again. It ignores you – it is already working.
- Out of Stock: You press the dispense button. It refunds your money and tells you to try later.
Same buttons. Completely different behavior. That is the State Pattern in a nutshell.
But let’s look at something you will actually build as a developer.
Structure
- Context (Order): The object whose behavior changes. It holds a reference to the current state and delegates all state-specific work to it.
- State (OrderState): An interface that declares methods for each action the context can perform.
- Concrete States (PendingState, ProcessingState, ShippedState, DeliveredState): Each class implements the state interface and defines the behavior for that particular state. Transitions happen here – each state knows what the next and previous states are.
The critical thing to understand is that the Order class never has a single if or switch checking what state it is in. It just calls state.next(this) and the current state object handles everything.
Example in Java – Order Lifecycle
This is the kind of thing you run into all the time in e-commerce systems. An order moves through stages: pending, processing, shipped, delivered. At each stage, the same actions (move forward, move backward, print status) do completely different things.
OrderState Interface
public interface OrderState {
void next(Order order);
void prev(Order order);
void printStatus();
}
Three methods. That is all any state needs to implement. The Order reference gets passed in so the state can trigger transitions on the context.
PendingState
public class PendingState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ProcessingState());
}
@Override
public void prev(Order order) {
System.out.println("Order is already in its initial state.");
}
@Override
public void printStatus() {
System.out.println("Order is pending. Waiting for processing.");
}
}
A pending order can move forward to processing but cannot go back any further. Simple.
ProcessingState
public class ProcessingState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ShippedState());
}
@Override
public void prev(Order order) {
order.setState(new PendingState());
}
@Override
public void printStatus() {
System.out.println("Order is being processed.");
}
}
Processing can go forward to shipped, or back to pending if the order is cancelled. This is the only state where going backward makes real business sense.
ShippedState
public class ShippedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new DeliveredState());
}
@Override
public void prev(Order order) {
System.out.println("Order is already shipped. Cannot go back.");
}
@Override
public void printStatus() {
System.out.println("Order has been shipped. In transit.");
}
}
Once shipped, there is no going back. It can only move forward to delivered.
DeliveredState
public class DeliveredState implements OrderState {
@Override
public void next(Order order) {
System.out.println("Order is already delivered. No next state.");
}
@Override
public void prev(Order order) {
System.out.println("Order is already delivered. Cannot revert.");
}
@Override
public void printStatus() {
System.out.println("Order has been delivered. Done.");
}
}
The final state. Nothing moves forward, nothing moves backward. The order lifecycle is complete.
Order Class (Context)
public class Order {
private OrderState state;
public Order() {
this.state = new PendingState();
}
public void setState(OrderState state) {
this.state = state;
}
public void nextState() {
state.next(this);
}
public void prevState() {
state.prev(this);
}
public void printStatus() {
state.printStatus();
}
}
Look at how clean this is. The Order class has zero conditional logic. It just delegates everything to whatever state object it currently holds. When the state changes, the behavior changes automatically.
Putting It All Together
public class MainProgram {
public static void main(String[] args) {
Order order = new Order();
order.printStatus();
// Order is pending. Waiting for processing.
order.nextState();
order.printStatus();
// Order is being processed.
order.nextState();
order.printStatus();
// Order has been shipped. In transit.
order.prevState();
// Order is already shipped. Cannot go back.
order.nextState();
order.printStatus();
// Order has been delivered. Done.
order.nextState();
// Order is already delivered. No next state.
}
}
Output:
Order is pending. Waiting for processing.
Order is being processed.
Order has been shipped. In transit.
Order is already shipped. Cannot go back.
Order has been delivered. Done.
Order is already delivered. No next state.
The order flows through its lifecycle naturally. Each state enforces its own rules about what transitions are allowed. No giant switch statement in sight.
Key Components
- Context: Maintains a reference to the current state object. Clients interact with the context, not the states directly.
- State Interface: Defines the contract that all concrete states must follow.
- Concrete States: Each one encapsulates the behavior for a specific state and handles transitions to other states.
- Transitions: Owned by the states themselves. Each state decides what the next and previous states are.
When to Use the State Pattern
- An object’s behavior depends on its state, and it must change behavior at runtime.
- You have large conditional blocks (
if/elseorswitch) that depend on the object’s state. - State-specific behavior should be independent and easily extensible.
- You want to make state transitions explicit and manageable.
Advantages
- Eliminates conditional bloat: No more switch statements that grow with every new state.
- Single Responsibility: Each state class handles exactly one state’s behavior.
- Open/Closed Principle: Adding a new state means adding a new class. Existing states remain untouched.
- Explicit transitions: It is clear which state transitions are allowed and which are not.
Disadvantages
- More classes: Every state becomes its own class. For simple cases with two or three states, this might be overkill.
- State explosion: If you have dozens of states with complex transitions, the number of classes can grow quickly.
- Tight coupling between states: Concrete states often reference each other directly (e.g.,
PendingStatecreatesProcessingState), which can make refactoring harder.
State vs Strategy
These two patterns look almost identical in structure. Both use composition and both delegate behavior to interchangeable objects. The difference is about who controls the switching.
- State Pattern: The object transitions between states internally. The
PendingStatedecides to switch toProcessingStateon its own. The client just callsnextState()and the state handles the rest. - Strategy Pattern: The client or some external code chooses which strategy to use. The strategy object itself never switches to a different strategy.
Think of it this way – State is an automatic gearbox that shifts on its own. Strategy is a manual gearbox where you pick the gear.
Real-World Use Cases
- TCP Connection States: A TCP socket moves through states like LISTEN, SYN_SENT, ESTABLISHED, CLOSE_WAIT, and CLOSED. Each state handles incoming packets differently.
- Media Players: Play, pause, stop – the same buttons behave differently depending on the player’s current state.
- Workflow Engines: Document approval systems where a document moves through draft, review, approved, and published states.
- Game Characters: A character might be idle, walking, running, jumping, or attacking. Each state defines different animations, physics, and available actions.
- Order Management Systems: Exactly what we built above. Every e-commerce platform has some version of this.
Wrapping Up
The State Pattern lets you replace messy state-dependent conditionals with clean, focused state classes. Each state knows its own behavior and its own transitions. The context object stays simple – it just delegates and moves on.
If you find yourself adding another else if to check the current status of something, stop. Pull that behavior into a state class. Your future self will thank you when the next requirement comes in and all you need is one new class instead of surgery on a 200-line method.