Design a Vending Machine
Design a vending-machine LLD: session states, inventory reserve, coin box, change, and the jam path that must not swallow Alice’s cash or sell the last bag twice.
Page content
Alice walks up with a ₹10 note and a ₹5 coin. Chips in slot A4 cost ₹12. She inserts both, presses A4, and the coil turns. The bag catches on the spiral. The motor times out. The tray is empty.
If you already moved her ₹15 into the vault and decremented A4 to zero, she has neither chips nor change. The machine ate a sale that did not happen.
If you wait to decrement until a successful drop, the other keypad on this hallway machine — or Alice’s double-press — can sell the last bag twice. Bob’s select(A4) also saw qty = 1. Two motors, one bag.
This is an LLD interview: a session, a cash box, and one reserved item. It is not a smart-fridge fleet and it is not a payments platform.
The main design question is:
How do we accept money, reserve one item, dispense, and return change without a double-vend or swallowed cash?
We will start with if money >= price then vend and watch Alice lose ₹15. Escrow, a hold on the last bag, a change check, and a jam refund appear when that if is no longer enough. Same last-unit race as Design a Parking Lot. The ATM cousin — bank ledger plus a local cassette — is Design an ATM.
1. Clarify the problem
“Design a vending machine” can mean a snack spiral or a city of connected coolers. The questions that change the objects:
- Coins only, or coins and notes?
- One item per session, or a cart?
- Must we give change, or exact-change only?
- What happens when the hopper cannot make change?
- Sold-out: reject the press, or take the money and fail?
- Can she cancel after inserting and get the same notes back?
- One keypad, or two faces sharing one cabinet?
- Cash only, or card tap too?
If the interviewer gives no extra constraints, I would state:
Money Coins and notes: ₹1, ₹2, ₹5, ₹10, ₹20
Session One item, then settle
Flow Pay, then select (common snack machine)
Change Required when we can make it
Cannot make change Abort the select, refund, light EXACT CHANGE
Sold-out Reject; do not take a new payment for it
Cancel Allowed while money is still in escrow
Cabinet One inventory; two keypads are two sessions
Card / age gate Out of scope for v1; same reserve + dispense
Telemetry to a warehouse, dynamic pricing, and a phone app that “unlocks a fridge” are a different interview. Say so and stay on this cabinet.
2. Functional requirements
The system must:
- Accept coins and notes into a session and show a running balance.
- Let Alice pick one in-stock slot whose price is covered.
- Dispense that item and return change.
- Reject sold-out and insufficient-funds selections without taking extra money.
- Cancel from the money-collecting state and refund the session.
- Never vend the last bag twice, and never keep cash for a failed drop.
Not in v1: multi-item carts, coupons, planograms from a server, card authorization as the source of truth. Those sit on the same session and inventory. They do not change the reserve.
3. Non-functional requirements
This is not a 50k QPS problem. Interviewers want a state machine and a hold, not a cluster.
| Requirement | Target |
|---|---|
| Last item | Atomic reserve; no double-vend |
| Insert / select / cancel | Milliseconds; one process |
| Cash | Integer rupees (or paise); never a float |
| Concurrency | Two keypads, one remaining bag |
| Clock | Machine clock for jam timeout and idle refund |
One qty unit moves to RESERVED for this session, or it does not. Durability is optional until they ask to persist a sales log. A jammed bag that never dropped is not a sale.
Edge cases
- Alice inserts ₹15, selects ₹12 chips, the coil jams.
- Alice and Bob both press the last bag on a two-face cabinet (or she double-presses).
- Hopper has only ₹10 notes; she is owed ₹3.
- She inserts ₹10 for a ₹12 item, then cancels — or idles out with escrow still loaded.
- Slot
A4is empty; the lamp still looks lit. - Exact-change lamp is on; she inserts a ₹20 for a ₹12 item.
4. Scale
Say the numbers so nobody spends the interview on QPS.
Slots 20 – 60 in one cabinet
Qty per slot 0 – 15 bags
Keypads 1, or 2 faces sharing inventory
Inserts / minute a few at a busy station
Sessions / day hundreds, not billions
A scan of 60 slots is fine. The bug is two sessions reading qty = 1. Ten thousand cabinets is more of this design, not a log. Restock is off the vend path.
Station hallway
├── Cabinet 14 face A ← this design (one Inventory)
│ face B ← second Session, same Inventory
└── Cabinet 15 ← this design
5. Entities / what we store
Seven facts. The session is the lock on cash. The hold is the lock on the bag.
Product id, name, price
Slot id, productId, qty, reserved, status OK | JAMMED | DISABLED
Inventory slots; reserve / release / commit
Coin denomination, count
CoinBox escrow[], hopper[d], vault[d]
ChangeMaker canMake(amount), make(amount) -> coins | empty
Session id, face, state, inserted[], reservedSlot?, balance
VendingMachine faces share one Inventory and one CoinBox
VendResult item?, change[], status
Escrow is money Alice just put in. It is still hers until a drop succeeds. The hopper is the tubes we make change from. The vault is operator money after a successful sale. If escrow and vault are the same pile, a jam refund is “hope the hopper can recreate ₹15.” That is how cash gets swallowed.
A session is the source of truth for “this face has Alice’s ₹15 and maybe a hold on A4.” Slot qty is stock you update in the same critical section as the hold. The sold-out lamp is a count you can recompute from qty - reserved. If the lamp and the slot disagree, the slot wins.
IDLE
insert ₹10, ₹5
ACCEPTING_MONEY escrow = [10, 5] A4 qty=1 reserved=0
select A4
ITEM_SELECTED A4 reserved=1 escrow still hers
motor + drop sensor
DISPENSING
success
vault += escrow, hopper pays ₹3, A4 qty=0 reserved=0
IDLE
Session id (s_9f3a) is the lock on this face; A4 is what she pressed. Faces share slots. The product name is a label.
6. APIs or class methods
In LLD you may show a controller, not HTTP. Either is fine.
insert(face, money) -> SessionView
select(face, slotId) -> VendResult
cancel(face) -> Refund
status(face) -> SessionView
Insert
Request:
POST /v1/machines/m_14/faces/A/insert
{ "denomination": 10 }
Response:
200
{ "session_id": "s_9f3a", "state": "ACCEPTING_MONEY", "balance": 10, "escrow": [10] }
A second insert of ₹5 yields balance: 15. Insert in IDLE opens a session. Insert after a successful vend is a new session. Insert during DISPENSING is rejected; the motor owns the cabinet face.
Select
Request:
POST /v1/machines/m_14/faces/A/select
{ "slot_id": "A4" }
Response (ok, then the machine dispenses):
200
{ "status": "VENDED", "slot_id": "A4", "price": 12, "change": [2, 1] }
Response (cannot make change):
409
{ "error": { "code": "CANNOT_MAKE_CHANGE", "message": "Insert exact or cancel" },
"refund": [10, 5], "exact_change_lamp": true }
Sold-out is 409 SOLD_OUT and escrow is untouched. Insufficient funds is 409 INSUFFICIENT_FUNDS with the current balance. Already reserved for this session is the same VendResult or 409 IN_PROGRESS — pick one and keep select from reserving twice.
Cancel
POST /v1/machines/m_14/faces/A/cancel
{}
200
{ "refund": [10, 5], "state": "IDLE" }
Cancel is legal from ACCEPTING_MONEY. After a reserve, cancel must release A4 and refund. During DISPENSING, cancel is ignored; the drop sensor decides.
The sold-out lamps can read status. The motor must not. Assignment of the last bag goes through select(), not through a count Alice glanced at.
7. Start with a naive if money >= price — show both failures
The first design everyone writes:
function select(slot):
if session.balance >= slot.price and slot.qty > 0:
dispense(slot)
session.balance -= slot.price
slot.qty -= 1
return makeChange(session.balance)
throw REJECT
It is easy to draw and easy to break. Two ways, depending on when you decrement.
Decrement first, then spin the coil
A4 qty = 1
Alice: balance 15 >= 12, qty = 0, eat ₹15, motor jams
Alice: no bag, no ₹15, sold-out lamp on
You committed a sale the physical world refused. That is swallowed cash.
Decrement after a successful drop
A4 qty = 1
Face A Alice: see qty 1, spin coil
Face B Bob: see qty 1, spin coil
Both bags cannot exist. One motor vends air.
qty goes 1 → 0 → -1, or both think they won.
You sold the last unit twice. Same bug as two parking gates and one compact stall: read, then write, with no claim in between.
The naive function also has no cancel, no “can I make ₹3?”, and no escrow. makeChange after you already took the ₹15 is too late if the hopper is empty.
naive vend, no lock wrong (double-vend)
naive vend, decrement first wrong (jam eats cash)
naive vend, lot mutex only race-safe, still swallows on jam
reserve + escrow + then vend the interview answer
A mutex around select fixes the race and is what you say for one keypad. A Senior answer still names when money and qty move.
8. Pay then select vs select then pay
Two legal machines. Pick one out loud and keep every method on that story.
Pay then select (my default, snack spirals):
IDLE → insert* → select → dispense → change → IDLE
└── cancel ↗
She can feed a note before she has decided. The display shows a balance. Sold-out is a reject at press time. This matches what people already know.
Select then pay (some drink machines, many ticket kiosks):
IDLE → select (hold) → insert until price → dispense → IDLE
└── cancel releases the hold
The hold starts earlier. That is nicer for the last bag: Bob cannot take A4 while Alice is still fishing for a ₹5. The cost: an abandoned hold needs a timeout, and the first press on an empty slot does nothing useful.
I would not mix them. If money can arrive before a hold and a hold can exist before money, you now have two “current item” fields and a cancel matrix. Pay then select is enough for this interview. Reserve happens at select, which is the first moment we know which slot to lock.
pay then select common; hold is short (select → drop)
select then pay safer last-item; hold needs TTL
both do not, unless they insist on a cart
A cart is several holds and one settle — the booking problem. Stay on one slot.
9. State machine
One machine for the session. Slot qty is not a second copy of Alice’s mood.
insert
IDLE ─────────────────────► ACCEPTING_MONEY
▲ │ ▲
│ cancel│ │ insert
│ │ │
│ ▼ │
│ (still ACCEPTING)
│ │
│ │ select
│ │ enough money
│ │ reserve ok
│ │ can make change
│ ▼
│ ITEM_SELECTED
│ │
│ │ start motor
│ ▼
│ DISPENSING
│ / \
│ drop ok / \ jam / timeout
│ ▼ ▼
│ GIVING_CHANGE REFUNDING
│ │ │
└───────────────────┴──────────────┘
IDLE
IDLE has no escrow and no hold. ACCEPTING_MONEY has escrow only. ITEM_SELECTED has escrow and a reserved slot. DISPENSING is physical; software does not take more money and does not cancel.
Illegal transitions:
IDLE --select--> (nothing reserved, no money)
ACCEPTING --dispense--> (no item chosen)
DISPENSING --insert--> (motor owns the face)
GIVING_CHANGE --select--> (session already settling)
RESERVED slot without a session (leak)
PAID vault while still DISPENSING (swallow risk)
Cancel from ACCEPTING_MONEY returns escrow and goes to IDLE. Cancel from ITEM_SELECTED (she hit the cancel pad before the motor started) releases the hold, returns escrow, IDLE. That second cancel is the one interviewers forget. During DISPENSING you wait for the sensor.
Idle timeout is cancel. Same method. Do not invent a fourth money path.
BEGIN / synchronized (session + slot)
ACCEPTING → ITEM_SELECTED
slot reserved += 1
END
BEGIN / synchronized
drop success → commit qty, vault += escrow, pay change
drop fail → reserved -= 1, refund escrow
END
If you persist a SALE row and then the coil jams, you have a sale without a bag. If you decrement qty and forget the session, Bob’s face still sees a bag that Alice is about to drop. Illegal: qty < reserved, vault credit without a committed drop.
10. Reserve the last item
The claim must be one operation: this session now owns one unit of A4, or it does not.
Same pattern as the parking-lot CAS and a booking hold. Application if qty > 0 without a compare-and-set is not a proof.
In memory: hold count under the inventory lock
synchronized (inventoryLock):
slot = slots.get(slotId)
available = slot.qty - slot.reserved
if available < 1: throw SOLD_OUT
slot.reserved += 1
session.reservedSlot = slot
available is what the lamp should show. reserved is a unit that must not be sold again until commit or release.
commit(slot): qty -= 1; reserved -= 1 // drop succeeded
release(slot): reserved -= 1 // jam, cancel, cannot-make-change
Decrement-on-reserve plus restock-on-fail is the same invariant if you never display raw qty. I prefer an explicit reserved field so a crash dump still explains why the lamp said zero with one bag behind the glass (Alice is mid-vend).
Two faces, last bag
Alice (face A) Inventory Bob (face B)
| | |
|---- select(A4) --------->| |
| |<---- select(A4) ---------|
| | |
| A4 qty=1 reserved=0 |
| | |
| reserved 0→1, win | |
| | |
| | available = 0 |
| | SOLD_OUT |
|<-- ITEM_SELECTED --------| |
| |-------- 409 SOLD_OUT --->|
Both presses are in flight. The first hold writes reserved = 1. The second still asks for available >= 1 and loses. Bob’s escrow is untouched; he can pick B2 or cancel.
Double-press on one face is simpler: the session is already ITEM_SELECTED or DISPENSING. The second select does not call reserve again.
per-slot lock two different slots vend in parallel
cabinet lock simple; enough for 60 slots
SQL CAS UPDATE slots SET reserved = reserved+1
WHERE id = A4 AND qty - reserved >= 1
I would start with one inventory lock. I would not open a distributed lock “because two faces.” One cabinet is one writer set.
Zero rows on the SQL form means you lost. Retry is the next slot the customer presses, not a loop over the cabinet.
11. Change: greedy, then refuse
Alice is owed ₹3. That is a constraint, not a display detail. If we cannot produce ₹3, we must not start the motor.
Denominations are ₹1, ₹2, ₹5, ₹10, ₹20. That set is canonical: greedy — largest first — is optimal. Say that. If they give you ₹1, ₹3, ₹4, greedy can fail and you need DP. Do not pretend every coin system is greedy.
canMake(amount, hopper):
need = amount
for d in [20, 10, 5, 2, 1]:
take = min(need / d, hopper[d])
need -= take * d
return need == 0
make(amount, hopper):
if not canMake(amount): return empty
// same loop, decrement hopper[d]
canMake is a dry run. make mutates the hopper only after the drop succeeds — or, if you pay change after commit in one critical section, after commit and vault credit. Do not empty a tube speculatively during ITEM_SELECTED unless you can put the coins back on jam.
Cannot make change
Hopper is four ₹10 notes. She inserted ₹10 + ₹5 for ₹12 chips.
owed = 15 - 12 = 3
canMake(3) = false
do not reserve, or reserve-then-release in the same lock
refund escrow [10, 5]
light EXACT CHANGE
session → IDLE
I abort and refund, as stated. Keeping her ₹15 in ACCEPTING_MONEY and asking for exact is friendlier and is a product line — say it, then pick refund so select is atomic: either we will vend with change, or she has her cash back.
The exact-change lamp is derived: on if canMake(p - d) fails for common overpay amounts, or if tubes for ₹1/₹2 are empty. The lamp is not a second coin box. select still calls canMake. Alice and Bob both seeing the lamp off does not skip the check.
exact only, no hopper simplest machine; select requires balance == price
give change when possible this design
recycle notes into tubes after sale, escrow top-ups short tubes, rest to vault
Recycle is how real hoppers stay alive. Mention it. Model v1 as escrow → vault, change from hopper. Mixing her ₹10 into a ₹10 tube before the drop is how jam refunds become “I no longer have your note.”
Escrow vs vault
insert coin goes to escrow, not vault
cancel return escrow (same pieces if the acceptor can reverse; else hopper)
jam return escrow; hopper untouched
success escrow → vault (or recycle), hopper pays change
If the note acceptor cannot reverse a ₹10, refund is equivalent value from the hopper. Then canMake must cover the full refund, not only the change, before you accept a non-reversible note. That is the Senior cash-path. If they do not ask, keep reversible escrow.
Never use a float. 12.0 rupees will lie. Integer rupees here; paise if they add ₹12.50.
12. Jam: refund, do not keep the sale
The opening story is a failed physical transition, not a new money API.
DISPENSING
start coil
wait up to T ms for drop sensor
sensor: bag crossed the IR beam → success
timeout / motor stall / sensor dark → JAM
On jam:
synchronized:
inventory.release(A4) // reserved 1 → 0; qty still 1
coinBox.refund(escrow) // [10, 5] leave the machine
session → IDLE
slot.status may become JAMMED if the spiral is stuck
qty never went to zero, so the next customer can try — unless you mark JAMMED and disable A4 until an attendant clears the coil. I would disable after a jam. Selling “the bag that is hanging on the spiral” is a second double-vend.
Do not:
qty -= 1
vault += 15
// motor jams
// now invent a goodwill refund from the hopper
That is decrement-first. Hopper may not have ₹15. Alice’s notes are already vault. You have a support ticket, not a state machine.
Restock-plus-refund is the same as release if you decremented early: qty += 1 and refund. Prefer never decrementing until the sensor. The hold already stopped Bob.
Alice Machine
| select A4, reserved
| DISPENSING
| timeout
| release A4, refund [10, 5]
| A4 JAMMED or still qty=1
A bag that drops late, after you refunded, is a mechanical leak. Attendant inventory counts exist for that. Do not add a second select path to “catch” the late bag.
13. Class sketch
Ideas, not a framework dump. Faces share one VendingMachine.
Money amount: int // rupees
Coin denomination: int
Product id, name, price: Money
Slot id, product, qty, reserved, status
Session id, face, state, escrow: Coin[], reservedSlot?
ChangeMaker
canMake(amount, hopper) -> bool
make(amount, hopper) -> Coin[] | empty
CoinBox
insertToEscrow(coin)
refundEscrow() -> Coin[]
commitSale(price) -> Coin[] // vault += escrow, return make(change)
hopperSnapshot() -> counts
Inventory
tryReserve(slotId) -> Slot | empty // atomic; available >= 1
commit(slotId) // qty--, reserved--
release(slotId) // reserved--
markJammed(slotId)
VendingMachine
insert(face, coin) -> SessionView
select(face, slotId) -> VendResult
cancel(face) -> Refund
VendingMachine.select in one breath:
select(face, slotId):
session = sessions.require(face)
if session.state != ACCEPTING_MONEY: throw BAD_STATE
slot = inventory.get(slotId)
if session.balance < slot.price: throw INSUFFICIENT_FUNDS
changeDue = session.balance - slot.price
if not changeMaker.canMake(changeDue, coinBox.hopper):
refund = coinBox.refundEscrow()
lamp.on()
session.reset(IDLE)
throw CANNOT_MAKE_CHANGE(refund)
reserved = inventory.tryReserve(slotId)
if reserved is empty: throw SOLD_OUT
session.reservedSlot = reserved
session.state = ITEM_SELECTED
return dispense(session)
dispense is the only method allowed to call commit or to credit the vault. Keypad handlers do not touch Slot.qty. Tests fake ChangeMaker for denomination tables; they use two threads against a real tryReserve for the last bag.
dispense(session):
session.state = DISPENSING
dropped = motor.vend(session.reservedSlot, timeout)
if dropped:
change = coinBox.commitSale(session.reservedSlot.price)
inventory.commit(session.reservedSlot)
session.reset(IDLE)
return VENDED(change)
else:
inventory.release(session.reservedSlot)
inventory.markJammed(session.reservedSlot)
refund = coinBox.refundEscrow()
session.reset(IDLE)
return JAMMED(refund)
cancel is refundEscrow plus release if a slot is reserved. It must not run in DISPENSING.
14. Tests that matter
Skip getters. Test the invariant.
- Cannot make change. Hopper
{10: 4}. Insert ₹10 + ₹5,select₹12. Refund[10, 5], no reserve, no motor, lamp on.A4qty unchanged. - Last item race.
A4qty = 1. Two threadsselect(A4)with enough escrow. OneVENDEDorDISPENSING, oneSOLD_OUT.reserved + sold <= 1. - Cancel refunds. Insert ₹10, ₹5.
cancel. Escrow empty, vault unchanged, stateIDLE. - Cancel after reserve. If you allow cancel in
ITEM_SELECTED,reservedreturns to 0 and Bob can then buyA4. - Jam. Motor timeout. Refund full escrow.
qtystill 1 (or restocked). Vault unchanged. SlotJAMMEDis not sold again. - Double-press / insufficient / sold-out. Second
selectdoes not reserve twice. ₹10 for ₹12 keeps escrow and takes no hold.qty - reserved = 0rejects and I keep escrow so she can pick another slot. - Happy change. ₹15 − ₹12 = ₹3 →
[2, 1]from a stocked hopper. Idle timeout is cancel.
Tests (1), (2), (3), and (5) are the offer. If they only have time for two, run cannot-make-change and two threads on one bag.
15. Extensions
Stay on objects. Do not add a message bus.
Card tap. Authorization holds rupees at the bank; escrow is a payment intent, not coins. Reserve and dispense stay identical. A jam is a void, not a hopper refund. Failure to void is the ATM reversal problem — see below.
Age-restricted. Cigarettes or alcohol: an AgeGate before tryReserve. Fail closed. The hold must not start until the gate passes, or you leak a reserved bag while she hunts for ID.
Exact-change only hardware. ChangeMaker requires changeDue == 0. Multi-item cart is several reserves and one commitSale — timeout every hold. Snapshot price onto the session at reserve so a remote planogram poke cannot change a live vend.
16. Final architecture / object diagram
One cabinet. One or two faces. One inventory. One coin box.
Face A keypad Face B keypad
| |
| insert / select / cancel |
v v
+-----------------------------------------------+
| VendingMachine |
| sessions[A], sessions[B] |
+-------------+-----------------+---------------+
| |
+--------v--+ +-----v------+
| Inventory | | CoinBox |
| tryReserve| | escrow |
| commit | | hopper |
| release | | vault |
+-----+-----+ +-----+------+
| |
v v
Slot A4 ChangeMaker
qty, reserved canMake / make
|
v
Motor + drop sensor
insert IDLE|ACCEPTING → escrow += coin
select canMake? tryReserve → DISPENSING → commit+change | release+refund
cancel if not DISPENSING: release?, refund escrow → IDLE
Kafka, Redis, and a region pair are not in this picture. If those words appear, you left LLD.
Source of truth session (escrow, state, reservedSlot) + slot qty/reserved
Derived sold-out lamps, exact-change lamp, display balance
Ephemeral motor spin, IR beam, keypad debounce
Audit committed sales after a successful drop
17. Contrast with an ATM
Both machines take a request, move cash, and can jam. The source of truth for the customer’s money is different.
Vending ATM
local CoinBox is the money bank ledger is the money
hopper is change for this sale cassette is notes you may dispense
product inventory is the stock cassette is also stock (of cash)
escrow until drop authorize / debit, then dispense
jam → refund escrow jam → reverse the debit
no identity card + PIN; session is a bank session
A vending vault is the operator’s till. If we credit it before the bag drops, we stole from Alice. An ATM cassette is the bank’s till. If we debit Alice and then jam, we stole from Alice unless we reverse. Same physical failure, different ledger.
Do not design the vending machine as “a tiny ATM.” There is no account to debit. The shared lesson is: do not commit the ledger (vault or account) until the dispenser succeeds, or you must have a reversal that cannot itself swallow cash. Full ATM walkthrough: Design an ATM. Parking-lot sibling: last stall is last bag — Design a Parking Lot.
18. Interview-ready summary
How to walk through in 10–15 minutes
0–2 min. Alice ₹15, ₹12 chips, jam. Or two faces, last bag. LLD, not a fridge fleet.
2–4 min. Coins+notes, one item, pay then select, change, cancel.
4–6 min. Entities: Session, Slot, CoinBox (escrow / hopper / vault).
6–9 min. Naive if money >= price, both bugs, then the state machine.
9–12 min. tryReserve, greedy canMake, jam = release + refund.
12–15 min. Tests: change, race, cancel, jam. Card and age-gate as the same reserve. ATM = bank vs till.
Key decisions to remember
- This is a session and a hold, not a connected-cooler product.
- Pay then select, one item, and stay on that story.
- Escrow is hers until the drop sensor fires.
- Last unit is
reserved += 1whereqty - reserved >= 1, same cabinet lock or SQL CAS. - Decrement (commit) only after a successful drop; jam releases and refunds.
- Greedy change on canonical INR; if
canMakefails, abort and refund, lamp on. - Cancel from
ACCEPTING(and before the motor) refunds; ignore cancel while dispensing. - Exact-change and sold-out lamps are derived.
selectdoes not trust them. - Two faces share
Inventory. Two sessions, oneqty. - Card tap replaces escrow with a void; the hold does not change. Fleet scale is more cabinets, not a log.
Likely interviewer follow-up questions
- Pay first or select first? Why?
- Two keypads, one bag — prove only one vend.
- Hopper cannot make ₹3. What do you return?
- The coil jams after you took ₹15. Walk the rollback.
- Can she cancel after
selectbut before the motor? - Why not decrement qty on the button press? What if the acceptor cannot reverse her ₹10?
- How does this differ from an ATM dispense jam?
- Card tap? Age check? Exact-change-only hardware?
Senior-level points that differentiate the answer
- Name swallowed cash and double-vend before drawing classes.
- Split escrow, hopper, and vault. Mixing them is how refunds fail.
- Tie
tryReserveto parking-lot occupy and booking holds — sameWHEREtrick. - Call
canMakebefore the motor, not aftervault += 15. - Cancel during
DISPENSINGis not a third money path; the sensor is. - Snapshot price at reserve; a remote price poke must not change a live session.
- Mark a jammed slot unsalable. The hanging bag is not stock.
A 1–2 minute verbal answer
A vending machine is a session, a slot inventory, and a coin box. I take pay-then-select: inserts go to escrow, not the vault. On select I check funds, check that the hopper can make change, then atomically reserve one unit (
qty - reserved >= 1). That stops two faces from selling the last bag. The motor runs only after the hold. A drop sensor commits: qty goes down, escrow moves to the vault, hopper pays change. A jam or timeout releases the hold and refunds escrow — we never decrement permanently for a bag that did not fall. Cancel from accepting money is that same refund. If we cannot make change, I abort and refund and light the exact-change lamp. I would test cannot-make-change, two threads on qty 1, cancel, and jam. Card tap swaps coins for a void; the reserve is the same. This stays in one process. An ATM is the same jam problem against a bank ledger, not a local till.
For HLD framing around this, see the System Design Interview Complete Guide and the questions hub. The parking-lot sibling is Design a Parking Lot. The cash-dispense sibling is Design an ATM.
