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:

  1. See why if-chains on the state get messy.
  2. Use the State pattern: each state answers the buttons.
  3. Work out change and pay it in the fewest coins.
  4. Refuse fake coins, sold-out items and short payments.
  5. 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.

ClassIts one job
VendingMachineHolds the stock, the money in, and the current state
MachineStateInterface: what each button does in this state
IdleNo money in: select is refused; a coin moves to HasMoney
HasMoneyMoney 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:

InsertBalance beforeBalance afterReturned
5050
3553

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

MistakeWrongCorrectWhy it happens
If-chains on stateif (state === "idle") ... in every method.One class per state.It is quick for the first two states.
Losing moneyA sold-out selection resets the balance.Refuse and keep the money.The happy path never hits a sold-out slot.
Accepting fake coinsbalance += coin for any number.Check the coin is 1, 2, 5 or 10.Tests only insert real coins.
Greedy change everywhereBiggest-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 outsideThe 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:

Next: Seat Booking. You will hold and book cinema seats safely when many people click at the same moment.