Behavioral modeling
State machine vs status enum
Status is a label on an entity (CREATED, ACTIVE, PAID). The State pattern is for when each mode has real behavior of its own. Both model a machine; they are not the same tool size.
Example: a ticket has four chapters and almost no extra behavior — activate, pay, close. A vending machine has the same coin() and select() buttons, but idle, has-coin, and sold-out each do something different. Prefer an enum plus guards for the ticket; prefer State objects for the vending machine.
Ticket
Add a row
Small table, short commands, one class. CREATED → ACTIVE → PAID → CLOSED. Illegal CLOSED → ACTIVE is a missed row, not a missing class.
- Orders, bookings, invoices — same shape.
- Behavior is “may I?” not “do a policy.”
- New chapter = new enum value + one legal move.
Vending / ATM
Add a class
Same methods, different work per chapter. coin() and select() mean something else in idle versus sold-out. Elevator movement and ATM sessions belong here.
- Each state holds real code, not a one-line require().
- The current state picks the next state.
- New chapter = new type that implements the buttons.
Status enum + guards Ticket, Order, Booking small table, short commands, one class State objects Vending, ATM, Elevator same methods, different work per chapter Neither auth → balance → limit that is a pipeline of steps, not a life cycle
The test
If I add a state, do I add a class, or do I add a row to the table? Ticket: a row. Vending: a class. Pick the smaller tool that still makes illegal arrows impossible. A is enough when the states are labels and the commands are short. Reach for a when the in each chapter.
Do not use State objects to hide a workflow that is actually a of checks. Auth then balance then limit is a chain, not four statuses of the same account. Do not use a status enum when every branch is a page and the next product request will add two more chapters of behavior — you will drown in switch.
Ticket: status is enough
CreatedState, ActiveState, PaidState, ClosedState that only assign an enum are extra ceremony. A table and three commands are the design. CLOSED → ACTIVE still throws. You did not need eleven types to get there.
enum TicketStatus { CREATED, ACTIVE, PAID, CLOSED }
class Ticket {
private TicketStatus status = TicketStatus.CREATED;
void close() {
if (status != TicketStatus.PAID) throw new IllegalStateException();
status = TicketStatus.CLOSED;
}
}
// Adding REFUNDED: one enum value, one command, one legal row.
// You did not need a RefundedState class.Vending: behavior changes with the chapter
Idle.coin → HasCoin. SoldOut.coin → still SoldOut (reject). Each class holds the behavior. Ticket did not need this. When the switch on an enum grows a page per button, extract objects — and then open the State pattern note. This page only makes the choice.
interface VendingState {
VendingState coin(VendingMachine m);
VendingState select(VendingMachine m, String slot);
}
class Idle implements VendingState {
public VendingState coin(VendingMachine m) { m.addCredit(); return new HasCoin(); }
public VendingState select(VendingMachine m, String slot) { return this; /* ignore */ }
}
class SoldOut implements VendingState {
public VendingState coin(VendingMachine m) { m.refund(); return this; }
public VendingState select(VendingMachine m, String slot) { return this; }
}
// Same buttons. Different work. Ticket’s pay() is not this.Do not confuse State with Strategy
Both swap an object the context holds. Strategy is “how do I do this job?” — you pick nearest versus random, and the strategy does not appoint its successor. State is “what am I right now?” — Idle becomes HasCoin because a coin arrived. Pricing is Strategy. Vending sessions are State. Ticket’s enum is a tiny machine, not a strategy. See Strategy when the interviewer changes the fee, not when they change the chapter.
Use enum
Ticket, order, booking. Short guards. One table.
Use neither
Auth → balance → limit. A pipeline, not a life.
Common mistakes
State objects for a four-step ticket
CreatedState, ActiveState, PaidState, ClosedState that only assign an enum are extra ceremony. A table and three commands are the design. CLOSED → ACTIVE is still just a missing row.
A status enum when each state is a page of code
A switch that grows with every button on the vending machine is the smell. That is when states become objects — and when you open the State pattern note.
Calling a pipeline a machine
Four checks in a row are not four lives of the same object. Keep Chain of Responsibility (or a list of validators) for that shape.
How to choose: say it clearly
“Ticket is a status with a table. Vending would be State objects because coin() changes meaning. I am not starting with the pattern name.” That statement is the design decision. The machine and the table come first either way.
A switch that is already too fat
If insertCoin has four cases and each case is fifteen lines, you are past enum-plus-guard even if you still have an enum. Extract the cases into types. Keep the context thin: VendingMachine holds money and inventory, and delegates buttons to state. Ticket never reached that size. Do not extract early to look clever; extract when the next button would touch every case.
Elevator is the usual follow-up. idle, moving, boarding — open(), close(), step() mean different work. That is objects. A lift “status” string plus a 60-line move() is the smell this page exists to catch.
Ticket checklist
- Four labels, three short commands.
- Adding REFUNDED is one row.
- CLOSED → ACTIVE is a missed row, not a missing class.
Vending checklist
- coin() and select() change meaning per chapter.
- Adding SoldOut is a new type.
- A 40-case switch is the smell.
- Context stays thin; states appoint the next state.
When unsure, ship the enum. Promote to objects the moment a button’s meaning forks. Do not promote Ticket to look like vending.
Read next
Takeaway
Both Ticket and vending are machines. Ticket adds a row. Vending adds a class. Start with enum-plus-guard. Reach for State objects when the same button does different work. A pipeline of checks is neither. Illegal arrows stay impossible either way.