June 17, 2026 in patterns5 minutes
A intermediate-level guide to State: before-and-after java code and diagrams for a CS student.
Imagine you are building software for a vending machine. Initially, the logic is simple: if it is idle, take money; if it has goods, dispense them. But as requirements grow—adding “out of order” modes, maintenance states, or error handling—the code begins to buckle under its own weight.
class VendingMachine {
private int balance = 0;
private String state = "IDLE"; // Possible: IDLE, HAS_GOODS, SOLD
public void insertCoin(int amount) {
if (state.equals("IDLE")) {
balance += amount;
System.out.println("Balance: " + balance);
state = "HAS_GOODS";
} else if (state.equals("HAS_GOODS")) {
System.out.println("Already in use.");
} else {
System.out.println("Machine is out of order.");
}
}
public void pressButton() {
if (state.equals("HAS_GOODS")) {
System.out.println("Dispensing item...");
state = "IDLE";
balance = 0;
} else if (state.equals("IDLE")) {
System.out.println("Insert coins first.");
} else {
System.out.println("Cannot press button now.");
}
}
}This class suffers from “Switch Statements,” a code smell where a single method uses conditional logic to decide how to act based on an internal variable like state. As you can see in the diagram below, the behavior of insertCoin and pressButton is trapped inside large blocks that must check the current status every single time they are called.
flowchart TD
A[insertCoin] --> B{state == IDLE?}
B -- yes --> C[update balance/state]
B -- no --> D{state == HAS_GOODS?}
D -- yes --> E[print error]
D -- no --> F[print error]
G[pressButton] --> H{state == HAS_GOODS?}
H -- yes --> I[dispense/reset]
H -- no --> J{state == IDLE?}
J -- yes --> K[print error]
J -- no --> L[print error]
This approach creates “conditional complexity.” The problem isn’t just that the code is long; it is that the logic is fragile. If you want to add a new state—for example, REPAIR_MODE—you cannot simply add one piece of code. You must hunt through every single method in the VendingMachine class and add another else-if branch to ensure the machine behaves correctly in that new mode.
This violates the Open/Closed Principle: your classes should be open for extension but closed for modification. Currently, every time you extend the machine’s capabilities, you are forced to modify (and potentially break) existing, working code.
To fix this, we use a refactoring technique called Replace Conditional with Polymorphism. Instead of asking “What state am I in?” inside every method, we delegate the work to an object that represents that state.
In this design, the VendingMachine (the Context) holds a reference to a State interface. The specific behavior is moved into separate classes like IdleState and HasGoodsState. When you call insertCoin on the machine, it simply says: “Hey, current state object, you handle this.”
interface State {
void insertCoin(VendingMachine context, int amount);
void pressButton(VendingMachine context);
}
class IdleState implements State {
public void insertCoin(VendingMachine context, int amount) {
context.setBalance(amount);
System.out.println("Balance: " + amount);
context.setState(new HasGoodsState());
}
public void pressButton(VendingMachine context) {
System.out.println("Insert coins first.");
}
}
class HasGoodsState implements State {
public void insertCoin(VendingMachine context, int amount) {
System.out.println("Already in use.");
}
public void pressButton(VendingMachine context) {
System.out.println("Dispensing item...");
context.setBalance(0);
context.setState(new IdleState());
}
}
class VendingMachine {
private int balance = 0;
private State state = new IdleState();
public void insertCoin(int amount) {
state.insertCoin(this, amount);
}
public void pressButton() {
state.pressButton(this);
}
// Package-private for State implementation access
void setState(State state) { this.state = state; }
int getBalance() {
return balance;
}
void setBalance(int balance) {
this.balance = balance;
}
}By moving the logic into dedicated state classes, we achieve encapsulation. The VendingMachine no longer needs to know the rules for every possible scenario; it only needs to know how to talk to the State interface.
The following class diagram shows how the machine delegates its responsibilities to these specialized objects:
classDiagram
class VendingMachine {
-int balance
-State state
+insertCoin(int amount)
+pressButton()
void setState(State state)
int getBalance()
void setBalance(int balance)
}
class State {
<<interface>>
+insertCoin(VendingMachine context, int amount)
+pressButton(VendingMachine context)
}
class IdleState {
+insertCoin(VendingMachine context, int amount)
+pressButton(VendingMachine context)
}
class HasGoodsState {
+insertCoin(VendingMachine context, int amount)
+pressButton(VendingMachine context)
}
VendingMachine --> State : uses
IdleState ..|> State : implements
HasGoodsState ..|> State : implements
The magic happens during runtime. When a user interacts with the machine, a “handshake” occurs where the machine passes itself as an argument to the state object. This allows the state object to trigger transitions—like telling the machine to switch from IdleState to HasGoodsState.
sequenceDiagram
participant VM as VendingMachine
participant IS as IdleState
participant HGS as HasGoodsState
Note over VM, HGS: User inserts a coin
VM ->> IS: insertCoin(this, amount)
IS ->> VM: setBalance(amount)
IS ->> VM: setState(new HasGoodsState())
Note right of VM: The machine has now changed behavior!
This implementation is essentially a code-level version of a Finite State Machine (FSM). Each class represents a discrete node in the machine’s lifecycle, and the methods define the transitions between those nodes.
The State pattern is powerful, but it comes with a cost: “class explosion.” Because every new state requires a brand-new class file, you can end up with dozens of tiny classes that only exist to hold a few lines of logic.
Use the State pattern when:
if/else blocks are becoming unreadable.Avoid the State pattern when:
ON and OFF) that rarely change. A simple boolean flag is much more efficient and readable there.if/else chains with polymorphic calls to specialized state objects.Using the refactored VendingMachine context
VendingMachine vm = new VendingMachine();
vm.insertCoin(50);
vm.pressButton();