Object-Oriented Design Interview

Course Content

Object-Oriented Design Interview

14 sections · 29 lessons

Vending Machine: requirements and why State beats a flag


The vending machine is the textbook State pattern problem, and it is kept small on purpose so that one skill is tested without noise. This lesson covers the prompt, the requirements and invariants, and the argument for the State pattern — made the honest way, by writing the flag-and-if-else version first and watching it collapse.

Four questions that change the modelDesign avending machineWhich coins accepted?Must change be exact?Cards, or coins only?One item per session?
"Must change be exact" decides whether the machine may refuse a sale it could otherwise make.

Why this problem is kept small

A vending machine has four or five classes and no domain subtlety. That is deliberate: it is the textbook State pattern problem, and interviewers use it when they want to test one thing without noise. If your design handles the states cleanly, you pass; if it has a status field and a growing pile of if statements, you do not.

Because the domain is so small, the whole round becomes about lifecycle. Expect the interviewer to spend most of the time on transitions, and to attack with events out of order: "What happens if the user presses dispense before inserting money? What if they insert money with nothing selected? What if they cancel after paying?"

The four questions that change the model

1. "Which payment methods — coins only, notes, cards?" Coins and notes are synchronous: the money is in the machine the instant it is inserted. A card payment is asynchronous — you request authorisation and wait, and the machine needs a "waiting for authorisation" state and a timeout. Assume cash only for the core design and raise cards in the extensions; that is what the interviewer expects.

2. "Does it give change, and what if it cannot?" This is the question with real substance. If the machine cannot make exact change it must either refuse the sale before taking the money or refuse to complete it and refund. Assume yes, it gives change, and that the coin problem in the section's extensions is in scope.

3. "What happens if the user cancels mid-transaction?" Assume a full refund of whatever has been inserted. This creates the transition that makes the if-else design collapse, so ask it early.

4. "Is restocking and a maintenance mode in scope?" Usually out of scope for the core, but worth naming, because "the machine is being restocked" is another state and adding it to a finished design is a good extension question.

Assumptions this lesson makes

  • Cash only: a fixed set of coin denominations plus small notes.
  • The machine holds several item slots, each with a code, a price, and a quantity.
  • One item per transaction.
  • Change is given when possible; if it is not possible, the transaction is refused and the money returned.
  • One user at a time — a physical machine has one slot and one set of buttons. This makes the concurrency question different from the parking lot's, and the section's extensions address it.

Requirements

Functional requirements

Text
1. Select an item by its slot code2. Insert money, one coin or note at a time, accumulating a balance3. Dispense the item when the balance covers the price4. Return change5. Cancel at any point and refund everything inserted6. Refuse invalid actions clearly (sold out, insufficient funds, wrong order of operations)

Requirement 6 is unusual and it is worth writing down. In most designs, invalid input is an afterthought; here, the rejections are half the specification. "Dispense pressed with no item selected" is not an error condition to be handled somewhere generic — it is a defined behaviour of a specific state, and the interviewer will ask about it.

Rejections are half the specificationWhat the machine does• Accept coins and count the balance• Dispense the selected item• Return the correct changeWhat it must refuse, and how• Dispense pressed with nothing selected• Coins inserted for a sold-out slot• Selection made mid-dispense
Each rejection is defined behaviour of one specific state, not a generic error handler.

Non-functional requirements

N1. The machine must never dispense without full payment. A one-way correctness rule.

N2. The machine must never keep money without dispensing or refunding. The other direction, and the one people forget. Every path through the machine ends with either goods plus change, or a full refund. There is no third ending.

N3. Adding a state must not require editing every method. This is what forces the State pattern, below.

What is cut

Text
CUT (named, not forgotten)- Card and mobile payments (extensions)- Remote telemetry, sales reporting, and stock alerts to a back office- Multi-item purchases in one transaction- The physical hardware protocol — motors, sensors, coin validators

Treat hardware as an interface: Dispenser.release(slotCode) and CoinHopper.payOut(coins). Say so: "I'll model the hardware as two interfaces so the machine logic is testable without a machine." That is the same move Section 13 makes for the ATM, and it is worth a point in both.

The invariant to state out loud

There is one sentence that summarises N1 and N2 together, and stating it early sets up everything that follows:

"The invariant is that money and goods move together. At every moment, the machine's obligation to the user is either 'I owe you an item and change' or 'I owe you your money back' — and every transition has to preserve that."

An interviewer hearing that sentence in minute eight knows how the rest of the round will go.

The states, and why State beats a flag

Write the bad version first. It is the fastest way to understand what the pattern is for, and it is what your own first attempt probably looked like.

Attempt one: a status field and if-else

Java
public class VendingMachine {    private Status status = IDLE;        // IDLE, ITEM_SELECTED, PAID    private Item selected;    private Money balance = Money.ZERO;    public void selectItem(String code) {        if (status == IDLE) {            selected = inventory.get(code);            status = ITEM_SELECTED;        } else {            throw new IllegalStateException("item already selected");        }    }    public void insertMoney(Money amount) {        if (status == ITEM_SELECTED) {            balance = balance.plus(amount);            if (balance.isAtLeast(selected.getPrice())) status = PAID;        } else {            throw new IllegalStateException("select an item first");        }    }    public Item dispense() {        if (status == PAID) { … } else { throw new IllegalStateException("not paid"); }    }}

Three states, three methods, and it is readable. So far there is no problem.

Then the requirements arrive

Add cancel. Every method now needs to consider it, and cancel itself needs three branches — refund nothing from idle, refund nothing from selected, refund the balance from paid.

Add "dispensing". The motor takes two seconds and can jam, so the machine must reject input while it runs. That is a fourth state, and it must be handled in all five methods.

Add "out of service". Now five states.

Add "waiting for card authorisation". Six.

Here is what insertMoney looks like at that point:

Java
public void insertMoney(Money amount) {    if (status == OUT_OF_SERVICE)    { returnCoin(amount); return; }    if (status == IDLE)              { throw new IllegalStateException("select an item first"); }    if (status == DISPENSING)        { returnCoin(amount); return; }    if (status == AWAITING_AUTH)     { throw new IllegalStateException("card in progress"); }    if (status == ITEM_SELECTED) {        balance = balance.plus(amount);        if (balance.isAtLeast(selected.getPrice())) status = PAID;        return;    }    if (status == PAID)              { returnCoin(amount); return; }}

And this same ladder now exists in selectItem, dispense, cancel, refund, and serviceMode. Six states × six methods = thirty-six cases spread across six ladders, with nothing keeping them consistent. Adding a seventh state means finding and editing six methods, and the compiler will not tell you if you miss one. That is the cost, stated concretely — and it is the sentence to say in the interview.

The repair: one class per state

Invert it. Instead of each method knowing every state, each state knows every event.

Java
public interface State {    void selectItem(VendingMachine m, String code);    void insertMoney(VendingMachine m, Money amount);    void dispense(VendingMachine m);    void cancel(VendingMachine m);}

One class per state implements all four. IdleState.dispense rejects; PaidState.dispense releases the item. VendingMachine holds a current state and forwards every call to it, and its methods become one line each.

Three concrete gains, worth naming out loud:

  • Adding a state adds a file and edits nothing. That is Open/Closed (the SOLID reference).
  • The compiler enforces completeness. Implementing State means answering all four events, so no case can be silently forgotten.
  • Each state's rules are in one place, so "what does the machine do while dispensing?" is one file, not a search across six.
IDLEITEM SELECTEDPAYMENT RECEIVEDDISPENSINGREFUNDINGselectItem(code)insertMoney() ≥ pricedispense()item released + change returnedinsertMoney() <pricecancel()coins returnedout of stock, or cannot make changeEvery path returns to IDLE, and two of them return the user's money. A state machine that cannot refund is the answer that fails this question.
"Cannot make change" is a refund, not an error — modelling it as a state is what separates a working machine from a demo.