# Vending Machine

Course: [Low-Level Design](https://www.devtutorials.ai/learn/system-design/lld) · Section: LLD Case Studies
Source: https://www.devtutorials.ai/learn/system-design/lld/case-studies/vending-machine

> 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.

| 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 |

<LldDiagram caption="The class diagram for this design." code={"classDiagram\n  class VendingMachine {\n    -state: MachineState\n    -balance: number\n    -stock: Map\n    +insert(coin) number\n    +select(code) object\n    +refund() number\n  }\n  class MachineState {\n    <<interface>>\n    +insert(m, coin) number\n    +select(m, code) object\n  }\n  class Idle\n  class HasMoney\n  MachineState <|.. Idle\n  MachineState <|.. HasMoney\n  VendingMachine --> MachineState : current"} />

## 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:

Here is the same idea in code:

```ts runnable title="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:

```text
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:

```ts runnable title="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:

```text
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:

```ts runnable title="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:

```text
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:

```ts runnable title="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:

```text
[
  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:

```ts runnable title="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:

```text
{
  "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.

1. Make a `MachineState` interface with `insert` and `select`, and the classes `Idle` and `HasMoney`.
2. Let the machine ask its state for every button, and let states switch the machine to the next state.
3. In `HasMoney.select`, refuse sold-out items and short payments, keeping the money. Give fake coins straight back before asking the state.

Open the lab in the sandbox. Run the tests after each step. A failing test names the situation that broke, not the test code.

## Predict the Output

<CsPredict id="vend-1" code={"int amount = 18;\nvar coins = new List<int>();\nforeach (var c in new[] { 10, 5, 2, 1 })\n    while (amount >= c) { coins.Add(c); amount -= c; }\nConsole.WriteLine(string.Join(\"+\", coins));\nConsole.WriteLine(coins.Count);"} expected={"10+5+2+1\n4"} why="Take the biggest coin that fits each time: 10 leaves 8, 5 leaves 3, 2 leaves 1, and 1 leaves 0. That is four coins." />

## 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](https://refactoring.guru/design-patterns/state): the pattern used here.
- [Wikipedia: Change-making problem](https://en.wikipedia.org/wiki/Change-making_problem): when greedy change works.
- [TypeScript Handbook: Interfaces](https://www.typescriptlang.org/docs/handbook/2/objects.html): the MachineState interface.

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