Published
- 7 min read
State Pattern in Java: Why an ATM Is Not the Strategy Pattern
One class per state, each one returning the next, and the state machine you already run in production.
Your interviewer asks: an ATM behaves differently at every step, so is it the Strategy pattern?
If you said yes, that’s the trap. An ATM does change its behaviour: with no card it waits for one, with a card it waits for a PIN, with a PIN it lets you withdraw. But nobody switches it from outside. It changes on its own, and that is the State pattern. It has the same class diagram as the Strategy pattern, which is exactly why interviewers ask for the difference.
The if-else version
Most of us would code it like this. The ATM remembers its state, and every method checks it with an if-else:
void insertCard() {
if (state == IDLE) {
state = HAS_CARD;
} else if (state == HAS_CARD) {
say("card already in");
} else if (state == HAS_PIN) {
say("finish first");
}
}
enterPin() and withdraw() have the same if-else. That’s fine until you add a state like out of cash:
void insertCard() {
if (state == IDLE) {
state = HAS_CARD;
} else if (state == HAS_CARD) {
say("card already in");
} else if (state == HAS_PIN) {
say("finish first");
} else if (state == NO_CASH) {
say("out of cash");
}
}
Now you’re changing every method, and each one has to get the new branch right. The rules of one state are spread
across all of them: to see what HAS_PIN allows, you read three if-else chains.
The State pattern
Turn it around. Each state gets its own class, behind one interface:
interface AtmState {
AtmState insertCard();
AtmState enterPin();
AtmState withdraw();
}
Every method returns the state the ATM should be in next. The ATM just hands the work to its current state and keeps whatever comes back:
class Atm {
AtmState state = new Idle();
void insertCard() {
state = state.insertCard();
}
// enterPin, withdraw: the same
}
Each state decides what comes next. Idle moves on when a card goes in, and ignores the rest:
class Idle implements AtmState {
public AtmState insertCard() {
return new HasCard();
}
public AtmState enterPin() {
return this;
}
public AtmState withdraw() {
return this;
}
}
return this means “not allowed here, stay where you are”. HasCard and HasPin have the same shape. Out of cash?
One more class:
class NoCash implements AtmState {
public AtmState insertCard() {
return this;
}
// enterPin, withdraw: the same
}
Here’s the honest part: one existing line does change, because somebody has to send the ATM there. HasPin returns
NoCash when the last notes leave:
public AtmState withdraw() {
return --Atm.notes > 0
? new Idle()
: new NoCash();
}
Atm, Idle and HasCard stay as they are, and no method grows a branch. Run it and watch the ATM move through
its states:
/CodeVxDev
state
java State.java
7:21:30
Strategy vs State
This looks just like the Strategy pattern: one interface, a class per behaviour, and an object that holds one of them. The difference is who swaps:
// Strategy: the caller picks
checkout.strategy = payPal;
// State: it moves itself
state = state.insertCard();
In Strategy, your code picks how a job gets done, like how to pay at a checkout. In State, nobody picks. The states switch on their own:
/CodeVxDev
state
java WhoSwaps.java
7:21:30
This is the comparison interviewers ask for:
| Strategy | State | |
|---|---|---|
| Who changes the object | The caller (or the DI container), once | The object itself, again and again |
| The classes know others | No, strategies are independent | Yes, a state names the state that follows |
| The question it answers | “Which way of doing this job?” | “What am I allowed to do right now?” |
| The one line | checkout.strategy = payPal; | state = state.insertCard(); |
One system can use both. In an elevator system, the dispatcher that decides which car answers a call is a Strategy (nearest car, SCAN, zoned), while each car moving between idle, moving and doors open is a State.
You’ve probably built this in production
An order gets created, then paid, then shipped. You can cancel before it ships, but after that it’s too late. That is a state machine, and when it’s this simple, skip the classes and pack it into an enum:
enum Status {
CREATED, PAID, CANCELLED,
SHIPPED {
Status cancel() {
throw new TooLate();
}
};
Status cancel() {
return CANCELLED;
}
}
Each constant is a state, and SHIPPED overrides cancel() with its own rule, so there’s no if-else on the status
anywhere. TooLate is a small RuntimeException of our own.
/CodeVxDev
state
java OrderStatus.java
7:21:30
Many codebases go lighter still: the enum only says which status may follow which, and one method on the order checks every move:
enum Status {
CREATED, PAID, SHIPPED, CANCELLED;
boolean canMoveTo(Status next) {
return switch (this) {
case CREATED -> next == PAID
|| next == CANCELLED;
case PAID -> next == SHIPPED
|| next == CANCELLED;
case SHIPPED, CANCELLED -> false;
};
}
}
class Order {
Status status = Status.CREATED;
void moveTo(Status next) {
if (!status.canMoveTo(next)) {
throw new IllegalStateException(
status + " can't go to " + next);
}
status = next;
}
}
/CodeVxDev
state
java OrderFlow.java
7:21:30
That version is a transition table, which suits a status that only gates moves. When each state also behaves
differently, what withdraw() or cancel() actually does, give the behaviour to the state: constants with bodies,
or a class per state.
Before you ship it
A few things the short version leaves out:
- Shared instances.
new HasCard()on every call is wasteful. These states hold no data, so share one instance of each, as constants or as enum constants like the ones above. - Say no out loud.
return thisquietly ignores a call that isn’t allowed. In production, reject it with a clear message or an exception, so a client that enters a PIN with no card learns why nothing happened. - Store the state, not the class. The state is a column: store the enum (
@Enumerated(EnumType.STRING)in JPA) and hang the behaviour off it. Stateless state classes can be shared instances, or Spring beans looked up by status.
Spot it in LLD interviews
If the requirements read like a lifecycle (“first…, then…, only after…, can’t… once…”) and the same action does different things at different steps, model the steps as states:
| LLD question | The object with a lifecycle | States |
|---|---|---|
| Vending machine | The machine | Idle, has coin, dispensing, sold out |
| Elevator system | One car | Idle, moving up, moving down, doors open |
| Traffic signal | One light | Red, green, yellow |
| Movie or seat booking | A seat | Available, held (with a timeout), booked |
| Task board | A task | To do, in progress, in review, done |
| Food delivery | An order | Placed, accepted, cooking, picked up, done |
| Parking lot | A ticket | Issued, paid, exited |
| Document approval | A document | Draft, in review, approved, published |
In the interview, draw the states and the arrows between them first. Then show one state class, and say how a new state gets added: one new class, plus the one state that leads to it.
When not to use it
State earns its keep when an object moves through stages with rules of their own. With two states and a boolean, an if-else is simpler, and a class per state is just noise.
/CodeVxDev
state
cat state.md
7:21:30
The interview trap
An order just got paid, and email, inventory and analytics all need to know. Should the order call all of them and notify?
A. Yes, call them   B. No
Think about what happens when a fourth service needs to know. Leave A or B as a comment on the video; the next episode in this series answers it.