Vending Machine
By the Syncfusion DevTutorials team · Last updated
Rebuild a vending machine with the State pattern: coins, change, sold-out items and refunds, with hidden tests and a design check.
QuickBite’s office has a vending machine that sells drinks. The result of pressing a button depends on the machine’s current condition.
For example:
- A customer cannot buy a drink before inserting enough money.
- When enough money has been inserted, the machine can provide the selected drink and return any change.
- If the selected drink is unavailable, the machine must not complete the purchase.
In this case study, you will design the vending machine using the State pattern. A state represents the machine’s current condition, such as waiting for money, ready for a selection, or dispensing a drink. Each state decides how the machine should respond to an action.
In simple words: you will place the behaviour of each machine state in a separate class, so the same button can perform the correct action at the correct time.
What You Will Learn
In this case study, you will:
- See why if-chains on the state get messy.
- Use the State pattern: each state answers the buttons.
- Work out change and pay it in the fewest coins.
- Refuse fake coins, sold-out items and short payments.
- Rebuild the machine in the sandbox, with hidden tests and a design check.
Any object that behaves differently depending on what happened before is a state machine: an order, a vending machine, a traffic light, a payment. Draw the states and arrows first, then write one class per state.
Step 1: Agree on the Requirements
Before writing code, confirm what the vending machine must do.
What It Must Do
- Accept coins with values of 1, 2, 5, and 10.
- Return any unsupported coin immediately.
- Prevent item selection until money has been inserted.
- Keep the inserted money when the selected item is unavailable or the balance is too low.
- Dispense the selected item when enough money has been inserted.
- Return any remaining change after a successful purchase.
- Allow the customer to cancel the purchase and receive a full refund.
- Return to the idle state after a purchase or refund.
- Use separate state classes instead of checking the current state with a long if chain or switch statement.
What We Leave Out
- Running out of coins for change.
- Card or UPI payments.
- A maintenance mode (it would be one more state class).
Leaving things out on purpose is not lazy. It keeps the design small enough to finish, and you can say how you would add them later.
Step 2: Find the Classes
A simple trick: underline the nouns in the requirements. Most of them become classes. The verbs become methods.
| Class | Its one job |
|---|---|
| VendingMachine | Holds the stock, the money in, and the current state |
| MachineState | Interface: what each button does in this state |
| Idle | No money in: select is refused; a coin moves to HasMoney |
| HasMoney | Money in: more coins add up; select checks stock and price |
The class diagram for this design.
Step 3: Build the Key Parts
The Problem With If-Chains
The same vending-machine action can produce different results depending on the machine’s current state.
For example, when the machine is idle, selecting a drink should ask the customer to insert money. After enough money has been inserted, selecting an available drink should dispense it and return any change.
One way to handle this is to use a long chain of if statements that checks the current state before every action. As more states and rules are added, this code becomes difficult to read, test, and update.
The State pattern avoids this problem by placing the behaviour of each state in a separate class. The machine passes each action to its current state, and that state decides what should happen.
The figure shows the if-chain as a grid. Each button must check each state:
Two states and two buttons make four branches. A third state adds a branch to every button.
Here is the same idea in code:
The if-chain version: every button checks the state
let state = "idle";
let balance = 0;
function press(button: string): string {
if (state === "idle" && button === "select") return "insert money first";
else if (state === "idle" && button === "coin") { state = "has_money"; balance += 10; return "ok"; }
else if (state === "has_money" && button === "coin") { balance += 10; return "ok"; }
else if (state === "has_money" && button === "select") { state = "idle"; return "here is your drink"; }
else return "?";
}
console.log(press("select"), "|", press("coin"), "|", press("select"));
Expected output:
insert money first | ok | here is your drink
It works, but every new state (maintenance mode, out of change) means editing every branch.
The State Pattern
Make each state a class. The machine just asks its current state what a button does. Each state can move the machine to the next state.
Step through select, coin, select. The same select button gives two different answers:
The machine ends back in idle.
Here is the same idea in code:
The State pattern: each state answers the buttons
interface State {
readonly name: string;
coin(m: Machine): string;
select(m: Machine): string;
}
class Idle implements State {
readonly name = "idle";
coin(m: Machine): string { m.money += 10; m.state = new HasMoney(); return "ok"; }
select(): string { return "insert money first"; }
}
class HasMoney implements State {
readonly name = "has_money";
coin(m: Machine): string { m.money += 10; return "ok"; }
select(m: Machine): string { m.money = 0; m.state = new Idle(); return "here is your drink"; }
}
class Machine {
state: State = new Idle();
money = 0;
coin(): string { return this.state.coin(this); }
select(): string { return this.state.select(this); }
}
const m = new Machine();
console.log(m.select(), "|", m.coin(), m.state.name, "|", m.select(), m.state.name);
Expected output:
insert money first | ok has_money | here is your drink idle
Easy way to remember: the machine is a remote control, and the state is the TV it is pointed at. The same button does a different thing depending on which state is in charge.
Change
Change is what is left after the price. If the customer paid too little, the machine asks for the rest.
Here is the code:
Change: what is left after the price
function change(paid: number, price: number): number {
if (paid < price) throw new Error(`insert ${price - paid} more`);
return paid - price;
}
console.log(change(20, 15));
console.log(change(15, 15));
try {
change(10, 15);
} catch (e) {
console.log((e as Error).message);
}
Expected output:
5
0
insert 5 more
Fewest Coins
To give change, pay out the biggest coin that fits, again and again. With Indian coins (1, 2, 5, 10) this always gives the fewest coins.
Step through change of 18:
18 is paid as 10 + 5 + 2 + 1, and 7 is paid as 5 + 2.
Here is the same idea in code:
Give change with the fewest coins
function coinsFor(amount: number, coins = [10, 5, 2, 1]): number[] {
const out: number[] = [];
for (const c of coins) {
while (amount >= c) {
out.push(c);
amount -= c;
}
}
return out;
}
console.log(coinsFor(18));
console.log(coinsFor(7));
Expected output:
[
10,
5,
2,
1
]
[
5,
2
]
"Biggest coin first" does not give the fewest coins for every set of coins. With coins of 1, 3 and 4, an amount of 6 gives 4+1+1 (three coins) instead of 3+3 (two). Check your coin set before trusting the greedy rule.
Fake Coins
The machine accepts only coins of 1, 2, 5 and 10. Step through a real coin and a fake one:
| Insert | Balance before | Balance after | Returned |
|---|---|---|---|
| 5 | 0 | 5 | 0 |
| 3 | 5 | 5 | 3 |
Here is the same idea in code:
Only real coins are accepted
const COINS = [1, 2, 5, 10];
function insert(coin: number, balance: number): { balance: number; returned: number } {
if (!COINS.includes(coin)) return { balance, returned: coin };
return { balance: balance + coin, returned: 0 };
}
console.log(insert(5, 0));
console.log(insert(3, 5));
Expected output:
{
"balance": 5,
"returned": 0
}
{
"balance": 5,
"returned": 3
}
Lab: Build It
The starter works, but it is built on if-chains on state. Rebuild it with the State pattern. The hidden tests check the behaviour; the design check needs the if-chains gone.
Predict the Output
Common Mistakes
| Mistake | Wrong | Correct | Why it happens |
|---|---|---|---|
| If-chains on state | if (state === "idle") ... in every method. | One class per state. | It is quick for the first two states. |
| Losing money | A sold-out selection resets the balance. | Refuse and keep the money. | The happy path never hits a sold-out slot. |
| Accepting fake coins | balance += coin for any number. | Check the coin is 1, 2, 5 or 10. | Tests only insert real coins. |
| Greedy change everywhere | Biggest-coin-first for any coin set. | Check the coin set, or use a smarter method. | It works for the coins you know. |
| State set from outside | The UI sets machine.state = "idle". | Only states move the machine. | It is a quick fix for a stuck machine. |
Practice
What to Remember
- A machine whose buttons depend on the past is a state machine.
- Use the State pattern: one class per state.
- States move the machine to the next state.
- Refused sales keep the money; fake coins go straight back.
- Biggest-coin-first change works for 1, 2, 5, 10, but not for every coin set.
Further Reading
These are the sources used to check this case study:
- Refactoring.Guru: State: the pattern used here.
- Wikipedia: Change-making problem: when greedy change works.
- TypeScript Handbook: Interfaces: the MachineState interface.
Next: Seat Booking. You will hold and book cinema seats safely when many people click at the same moment.