Guide / low-level design

Inventory management system LLD

The LLD is where the inventory system stops being a diagram and becomes buildable: classes and tables, the methods that change stock, the sequence flows for receive and count — and the edge cases written as tests.

The key facts

  • The classes: Product, ProductVariant, Location, StockMovement (immutable), Stocktake, Reservation, PurchaseOrder, Order — each with its responsibilities and its forbidden operations.
  • The central method contract: every stock change funnels through one recordMovement() that validates, writes and never updates a balance.
  • The sequence flows to design: receive against PO, allocate-reserve, count-and-correct, cancel-and-release — each as a numbered interaction with its transaction boundary.
  • The companion levels: the HLD above and the schema walkthrough beside it.
  • Where it fits: this page is one branch of the inventory management guide.
Diagram — index of this pageThe ground this page covers
  1. 01The key facts
  2. 02Classes and responsibilities
  3. 03The sequence flows, with transaction boundaries
  4. 04The edge cases, written as tests

Classes and responsibilities

Product / ProductVariant — identity data, cost, unit; no quantity fields anywhere on them. Location — code, name, type. StockMovement — immutable value object: variant, location, signed quantity, timestamp, actor, reason, reference (PO, order, stocktake, adjustment). Construction validates: non-zero quantity, known variant and location, non-empty reason. InventoryService — the only writer: recordMovement() inside a transaction, plus query methods onHand(variant, location) summing movements and onOrder(variant) summing open PO lines. Reservation — variant, location, quantity, order reference, state (held, released, consumed). Stocktake — header plus count lines; closing it generates correction movements. PurchaseOrder / Order — stateful documents whose transitions are enforced in their service classes, not in the UI.

The design contract that reviewers should demand: no class outside InventoryService can persist a stock change. Every other path — portal, scanner, import — calls the same service, which is why the record stays one truth.

Genuine BSimple screenThe BSimple reorder view showing what to buy before stock runs out.
The BSimple reorder view showing what to buy before stock runs out.

The sequence flows, with transaction boundaries

Receive against PO. Validate PO state → for each line, write an inbound movement referencing the PO → update PO line received-quantity → commit. Partial receipts simply repeat; the PO tracks its own balance. Allocate. Begin transaction → lock affected stock rows (SELECT ... FOR UPDATE) → verify on-hand minus held reservations covers the request → insert Reservation → commit. The lock is the answer to the last-unit race; without it, two allocations both succeed and the record lies. Count and correct. Stocktake captures counted quantities → on close, each variance writes a correction movement with reason "count" and the stocktake reference → derived figures shift, history intact. Cancel and release. Order cancellation → held reservations flip to released in the same transaction that cancels the order → freed stock is promiseable immediately — no orphaned holds.

Each flow ends with the same property: on-hand still equals the signed sum of movements. The LLD companion for the order side adds the order-machine details.

DiagramDiagram: spreadsheet data imported into live stock records.
Diagram: spreadsheet data imported into live stock records.

The edge cases, written as tests

The LLD is finished when its edge cases exist as executable tests: two threads allocating the final unit (one wins, one rejected cleanly); a cancellation after partial consumption (release the remainder only); a count closing while a receipt is mid-transaction (serialise or queue — decide and test); a movement attempted for an unknown variant (refused at validation, never persisted). Write these before the UI exists, because they define the service contracts every client will depend on. This is also the honest moment of choice: a design this far along can be built — with the maintenance tail priced honestly — or used as the evaluation script for a maintained system. We build BSimple, so weigh that: it implements this LLD as production cloud software from $180/month AUD, and the trial lets you test the implementation against your own products rather than trusting the blueprint.

DiagramOrderPick and packInvoiceXero
Diagram: Order → Pick and pack → Invoice → Xero — how this work moves through BSimple.

Frequently Asked Questions

What is LLD in an inventory management system?

The low-level design: classes and their exact responsibilities, table schemas, method contracts, sequence flows with transaction boundaries, and the edge cases as tests. It sits below the HLD — which chose the components — and above the code.

Which class should own stock changes?

Exactly one — an inventory service whose recordMovement() validates, writes and commits. Every client path funnels through it, which is what keeps the record one truth and the audit trail complete.

How is the oversell race handled at LLD level?

Database serialisation inside the allocation transaction: lock the affected stock rows, re-verify availability against held reservations, insert the reservation or reject. Application-level check-then-write is the bug, not the fix.

How are stocktake corrections modelled?

As movements: closing a stocktake writes one correction movement per variance, referencing the stocktake, with the variance as the reason. The derived on-hand shifts; the history shows exactly why it shifted.

Where can I see an LLD like this running in production?

BSimple implements the movement-based model — one service, immutable movements, deliberate approvals — as maintained cloud software. The trial is the full product: probe the flows above on your own products.

In practiceCustomers ordering through their own portal link.
Customers ordering through their own portal link.

BSimple

Get started with BSimple