Inventory Features and Flows
Developer Resources Inventory Inventory Features and Flows Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the Inventory module.
Use this page for the inventory domain: what it does for users, admins, workers and systems, and how each flow behaves from start to finish.
Source Type Files or Docs What Was Extracted Technical design inventory-write.service.ts, inventory-reservation.service.ts, inventory-ledger.service.ts, inventory-math.util.ts, inventory.constants.tsGuarded UPDATEs, reservation lifecycle, generated columns, caps API admin/stock/*.controller.ts, admin/job/*.controller.tsRoute surface, permissions, rate limits Backend inventory-availability.service.ts, inventory-projection.service.ts, workers/*, import-export/*Side effects, projections, job lifecycle Schema packages/db/src/schema/inventory/*.tsFour tables, CHECKs, generated columns Jobs packages/jobs/src/index.tsInventoryJob names, payloads
Field Value Module inventorySubmodule N/A (single domain: stock, reservations, ledger, jobs) Primary user value Trustworthy stock: availability computed by the database, reservations with a full lifecycle, an auditable movement ledger, and admin adjustment/import/export Actors Admin, worker/system. No guest surface — stock reaches the storefront via the product response's inventory group Main entry points /api/admin/inventory, /api/admin/inventory/jobs, INVENTORY queue, outboxMain outputs Inventory responses, movement ledger, reservations, CSV imports/exports, MongoDB activity projections Related docs API , Backend
Actor Can Do Cannot Do Auth Requirement Notes Admin List inventory, view one product's row, update configuration, adjust stock, view movements, bulk adjust, submit/cancel import/export jobs Write available_quantity/stock_status (GENERATED), disable oversell while in debt, re-enable tracking on a row in debt Admin JWT + Inventory_READ / Inventory_UPDATE Cursor-paginated reads; ADMIN_WRITE 10/min, ADMIN_BULK_WRITE 5/min, ADMIN_ASYNC_JOB_SUBMIT 10/hour Worker/system Run import/export jobs, sweep expired reservations, reconcile drift, project activity — BullMQ worker Lease-based claim; cooperative cancellation; outbox enqueued Product domain (system) Create inventory rows on product create/import; read availability for responses Update counters outside InventoryWriteService Internal service InventoryWriteService is the only permitted writer
Capability Surface Actor Route/Trigger State Read State Written Linked API Section List inventory Admin Admin GET /api/admin/inventoryRows — API §2 One product's inventory Admin Admin GET /api/admin/inventory/products/:productPublicIdRow — API §2 Update configuration Admin Admin PATCH /products/:productPublicId/configurationRow + version track/oversell/threshold API §2.3 Adjust stock Admin Admin POST /products/:productPublicId/adjustRow Counters + movement + projection API §2.2 View movements Admin Admin GET /products/:productPublicId/movementsLedger — API §2 Bulk adjust Admin Admin POST /bulk/adjustRows Counters + movements API §2.4 Import/export jobs Admin/worker Admin → system /api/admin/inventory/jobsJob rows Job rows, CSV API §2.1 Reserve / release / finalize System (future cart/checkout) Internal service calls Row + reservations Counters + reservation + movement Reservations Sweep expired reservations Worker System cron → SWEEP_EXPIRED_RESERVATIONS Expired rows Settled + movements backend §9 Reconcile drift Worker System cron → RECONCILE Counters vs ledger correction movementsbackend §9 Project activity Worker System outbox → PROJECT_ACTIVITY Movements MongoDB backend §9
There is no guest-facing inventory flow. Stock reaches the storefront through the product response's inventory group (batched, one query per page) — see the products docs. All flows below are admin or system flows.
An admin corrects a stock level — a damage write-off, a physical count fix, a restock. The adjustment is a guarded UPDATE; a refused change (would go negative without oversell) returns a 409 and changes nothing.
Branch Condition Behavior Error/Result Negative result total_quantity would go negative without oversellRefused 409 INVENTORY_ADJUSTMENT_WOULD_GO_NEGATIVE Untracked row No row / track_inventory = false First adjustment auto-enables tracking Accepted Oversell allowed allow_oversell = trueNegative allowed (units owed) Accepted Duplicate request Same adjustment twice Two distinct movements (no idempotency on adjust) Two ledger rows
Reserve — idempotent via the globally unique reservation_key (duplicate insert is a no-op).
Release — idempotent via a status = 'active' guard; release twice is success (the caller's intent is "make sure this hold is gone").
Finalize — the same guard, but finalize twice is a 409 INVENTORY_RESERVATION_ALREADY_SETTLED (the caller's intent is a one-time event).
Expire — default TTL 15 minutes; a per-minute cron sweep settles expired rows and returns the units.
PATCH /products/:productPublicId/configuration — trackInventory, allowOversell, lowStockThreshold, version. Guards: re-enabling tracking on a row in debt is refused; disabling oversell while units are owed is refused (409 INVENTORY_OVERSELL_DISABLE_BLOCKED); stale version → 409 INVENTORY_VERSION_CONFLICT.
POST /bulk/adjust — up to 100 items (409 INVENTORY_BULK_LIMIT_EXCEEDED); per-item result { succeeded, failures }, each row naming the product AND the variant the item resolved to; an item may name a variantPublicId and means the default when it does not; writes ordered by variant_id ascending to avoid deadlock (reproduced as SQLSTATE 40P01 without the ordering). An unresolvable product or variant is a per-item failure, never a rollback of the items beside it.
CSV import of adjustments (only .csv accepted; required productPublicId + quantityDelta); export of the ledger; job lifecycle with lease claim, cooperative cancellation, zero-row export = success with header-only file.
Entity From Event/Action To Guard Condition Side Effects inventory_reservation— reserve activereservation_key unused globallymovement reservation_created inventory_reservationactiverelease releasedstatus = 'active' matchmovement reservation_released inventory_reservationactivefinalize finalizedstatus = 'active' matchmovement reservation_finalized; 409 if already settled inventory_reservationactiveexpire expiredTTL passed, sweep claim movement reservation_expired
Flow DB Writes Cache Effects Jobs Realtime Analytics Notifications Adjust Counters + movement (same tx) inventory domain outbox → projection — MongoDB activity (async) — Reserve/release/finalize/expire Counters + reservation + movement inventory domain outbox → projection — MongoDB — Configuration Config fields (+ movement if counters change) inventory domain — — — — Reconcile correction movementsinventory domain — — — — Import/export Job row + rows/CSV inventory domain outbox — — —
Scenario Trigger User/System Experience Recovery Source Guard refused Adjustment would go negative 409, nothing changed Adjust within limits or enable oversell inventory-write.service.tsOversell disable blocked Debt exists 409 Resolve debt first math util Deadlock Multi-product write unordered SQLSTATE 40P01 Product_id ordering prevents probe Job cancelled mid-import Admin cancel Cooperative abort, rollback Resubmit inventory-job-lifecycle.service.tsDrift detected Counters ≠ ledger correction movementHourly reconcile inventory-reconcile.processor.tsExport too large > 50,000 rows Job fails Narrow filters export service
Actor capability diagram — §3/§4.
Sequence diagram per major flow — §5.1, §5.2.
State machine diagram — §5.2/§7.
Data side-effect diagram — §9.
Error branch diagram — §6.1/§10.
Feature Minor Behavior Actor Trigger User/System Result Backend Side Effect Source Adjust Auto-enable tracking Admin First adjustment on untracked row Row becomes tracked Config flip in same tx inventory-write.service.tsAdjust Reason enum Admin reason fieldMovement kind Enum validation dto Reserve TTL override System ttlSeconds paramCustom expiry Business timestamp reservation service Reserve Key collision after expiry System Same key reused 409 — key global, not partial INVENTORY_RESERVATION_KEY_CONFLICTList Cursor pagination Admin cursorNext page Keyset Reconcile Heals missing rows Worker Hourly Products without rows get rows Compare-and-set Sweep Batch bounds Worker Per-minute Settles expired in batches INVENTORY_RESERVATION_SWEEP_BATCH_SIZEconstants Projection pre_order freezeSystem Tracked product in pre-order Never overwritten Projection rule math util
Rule Business Reason Actor Impact Enforced In API Impact Backend Impact Tests available_quantity/stock_status GENERATEDAvailability cannot go stale Admin sees computed truth DB GENERATED ALWAYS AS Read-only fields 428C9 on write attempt probe Negative legal under oversell Backorders = units owed Admin can over-adjust Conditional CHECKs 409 otherwise allow_oversell OR NOT track_inventoryprobe One guarded UPDATE per counter change No read-then-write races Refused ops return 409 Service 409 codes rowCount = 0 = refusedspec Multi-row ordering by variant_id Deadlock prevention Bulk adjust works Service sort Ordering by product_id leaves two variants of one product unordered Reproduced 40P01 probe + int spec Reservation key global + permanent No double-hold by replay System callers Unique index 409 on reuse Not partial spec Write monopoly InventoryWriteService onlyConsistent rules Service architecture — Reviewed spec pre_order never overwritten by projectionPre-order status is admin truth Product stock stays Projection rule — projectToProductStockStatusspec
Product Decision User Benefit Engineering Benefit Alternative Tradeoff Risk Generated columns Trustworthy availability No stale-write path Service-computed DB-version coupling Documented Ledger in PostgreSQL same-tx Auditable, atomic No dual-write gap Mongo ledger Postgres growth Retention via maintenance Analytics async via outbox Fast writes Decoupled Sync Mongo write Eventual Accepted Oversell opt-in per variant Backorders supported Explicit config Global flag Per-variant debt Guards Untracked default Existing products unchanged Safe backfill Track-all migration Manual enable Auto-enable on first adjust
Flow Edge Case Trigger Expected Behavior User/System Feedback Source Adjust Empty delta quantityDelta missingValidation error 400 dto Adjust Zero delta 0No-op movement? (validated) Accepted/rejected per dto Reserve Insufficient stock No oversell, not enough Refused 409 INVENTORY_INSUFFICIENT_STOCK Reserve Duplicate key Same key twice Second is no-op Same reservation unique index Finalize Twice Second finalize 409 INVENTORY_RESERVATION_ALREADY_SETTLEDSweep Race with release Sweep claims a row being released Idempotent per row One settles guarded UPDATE Reconcile Counters vs ledger disagree Drift correction movementLedger records Import Row invalid Bad row All-or-nothing Job failed, nothing written Export Zero rows Empty filter Header-only file Success Tracking re-enable Row in debt Config change Refused 409 canEnableTracking
Flow Reads Writes Cache Jobs/Events Response Fields Adjust inventory rowcounters + inventory_movement invalidate outbox → projection inventory response Reserve row + reservations reservation + counters + movement invalidate outbox → projection reservation List rows — inventory domain — cursor list Movements ledger — — — cursor list Import rows by productPublicId counters + movements + inventory_job invalidate outbox → import job