Inventory Management
Lots, reservations, picking order and an audit trail
Design a system that tracks stock per product and warehouse, promises units to orders without overselling, picks the right lot first, moves stock between sites and keeps a record of every change.
Overview
An inventory system answers one question all day: how many units of this product can I promise right now, and where are they? Goods arrive from suppliers in lots, orders reserve units, pickers take them from shelves, trucks move them between warehouses, and someone has to be told when a shelf runs low. Every one of those steps changes a count, and the counts must never lie.
The interview version strips the warehouse down to a few objects: a product identified by its SKU, a lot that arrived together (with an expiry date if the product has one), a warehouse that holds lots, and a movement record for each change. The hard parts are the same as in a real system. Two orders must not both take the last unit. Milk that expires first must leave first. A transfer must not lose or double units. And an auditor must be able to add up the history and get the shelf count.
A strong answer says 'available, not on hand' early, puts one lock around each warehouse's check-and-change, makes the picking rule a pluggable strategy, and writes the audit row in the same step as the change. The code below does all four.
Scoping the problem
Before drawing anything, ask these questions, because each answer changes a class.
- One warehouse or many? — Many means transfers and a lock per site — Many
- Do products expire or need lot tracking? — Decides whether stock is a number or a list of lots — Lots, with an optional expiry date
- Do orders reserve stock before shipping? — Reservation is what stops overselling between order and pick — Yes: reserve, then ship
- Which lot leaves first? — FIFO for most goods, FEFO for perishables — A strategy object; FEFO in the demo
- Who needs to know about low stock? — Polling versus notification — Listeners called at the reorder point
- What must the audit trail prove? — Whether a running total is enough or every movement is kept — Every movement, signed, in order
- Units: whole items, or weights? — Decimals need care — Whole units, integers
Reserve, then ship
An order does not take units off the shelf the moment it is placed; it reserves them. The reservation lowers what is available to the next order but leaves the lots alone, so a canceled order costs nothing. Shipping turns the reservation into real picks from real lots.
Available is not on hand
The two numbers drift apart as soon as an order is placed. Overselling bugs come from checking the wrong one.
- receive L1 (30) and L2 (30) — 60 — 0 — 60 — lots on the shelf
- reserve 40 — 60 — 40 — 20 — nothing left the shelf
- reserve 21 — 60 — 40 — 20 — refused: 21 is more than 20
- ship the 40 — 20 — 0 — 20 — picks come off the lots
Which lot leaves first
The picking rule is a strategy object with one method, order(lots). The inventory sorts the lots with it and takes from the front until the quantity is met. Adding a rule is one class.
- FIFO, first in first out — arrival sequence — most goods; steady stock rotation — ignores expiry dates
- FEFO, first expired first out — expiry date, then arrival; no expiry last — food, medicine, chemicals — a late delivery with a short date jumps the queue, which is the point
- LIFO, last in first out — arrival sequence, newest first — bulk piles where the top is the newest — old stock can sit for years
The demo receives L1 (expires Oct 20) and then L2 (expires Oct 12). A ship of 40 under FEFO empties L2 first.
- L2 — 2026-10-12 — 30 — 30 — 0, removed
- L1 — 2026-10-20 — 30 — 10 — 20
The audit trail
Every change appends one Movement row: a sequence number, the kind, the SKU, the warehouse, the lot, a signed quantity and who did it. Rows are never edited. A transfer is two rows, an out and an in, so the sum over all rows for a SKU always equals the units on hand across all warehouses. That equation is the auditor's check, and the demo asserts it.
- 2 — receive — W1 — L1 — +30 — ana
- 4 — receive — W1 — L2 — +30 — ana
- 6 — ship — W1 — L2 — -30 — ben
- 7 — ship — W1 — L1 — -10 — ben
- 8 — transfer_out — W1 — L1 — -5 — ben
- 10 — transfer_in — W2 — L1 — +5 — ben
Sum: 30 + 30 - 30 - 10 - 5 + 5 = 20, which is 15 in W1 plus 5 in W2. Sequence numbers come from one counter shared by lots and movements, so the gaps are normal.
One lock per warehouse
Every check-and-change on a warehouse runs under that warehouse's lock, so two reservations for the last units cannot both pass the check. A transfer needs two warehouses, and taking two locks is where deadlocks come from: one thread takes W1 then W2 while another takes W2 then W1, and each waits forever. The code sorts the two warehouses by id and always locks the smaller id first, so every thread agrees on the order. It also refuses a transfer from a warehouse to itself, which would try to take the same lock twice. Python's Lock is not reentrant, so the private helpers that run under the lock never take it again.
- receive, reserve, ship — the one warehouse — n/a
- transfer W1 to W2 — W1, W2 — sorted by id: W1 first
- transfer W2 to W1 — W2, W1 — sorted by id: W1 first, the same
What you will be able to do
- Explain available versus on hand and show, with a table, how reservations stop overselling.
- Write reserve and ship under one lock per warehouse and argue why the race is closed.
- Make the picking rule a strategy and trace FEFO on two lots.
- Keep a signed movement log and state the equation an auditor checks.
- Take two locks in a fixed order for a transfer and say why the order prevents deadlock.
Functional requirements
- receive(sku, warehouse, qty, lot_id, actor, expiry) adds a lot and writes a receive movement
- reserve(sku, warehouse, qty) promises units to an order and fails with InsufficientStock if fewer are available
- ship(reservation, actor) takes the promised units from lots in strategy order and returns the picks
- transfer(sku, from, to, qty, actor) moves units between warehouses, keeping lot identity and expiry
- The picking order (FIFO, FEFO) is a strategy chosen at construction
- A listener is called when a shipment brings a SKU to or below its reorder point
- Every change appends an immutable movement row with a signed quantity and the actor
Non-functional requirements
- Stock is never oversold: reservations plus shipments never exceed what is on hand, under concurrent callers
- Every operation on one warehouse is atomic under that warehouse's lock
- Transfers take two locks in a fixed order and cannot deadlock
- The movement log for a SKU sums to the units on hand across all warehouses
- Adding a picking rule or an alert channel adds a class and changes no existing one
Interview questions
- Two orders arrive at the same moment for the last 3 units. How do you make sure only one gets them?
- How does the picking strategy work, and how would you add a new rule?
- A transfer touches two warehouses. How do you avoid a deadlock?
- Why keep every movement instead of just updating a count?
- How would this change with millions of SKUs and many services?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio