Architecting an Enterprise Inventory Service for Commodities(raw materials)
In the software world, building an inventory app looks like a weekend project. You create a Products table, add a quantity column, and subtract 1 when someone buys something.
But if you are building software for a manufacturing or logistics company dealing in commodities like raw plastics, steel, chemicals, or textiles that naive approach will collapse on day one.
Commodities don’t behave like standard e-commerce products. You aren’t selling single, distinct items with a barcode. You are dealing with bulk materials measured in kilograms, liters, and tons. The prices fluctuate, the materials expire, and they arrive in massive shipments from suppliers.
Here is the technical blueprint for architecting a bulletproof, ERP grade Inventory Service specifically designed for commodities.
The Unit of Measure (UOM) & Precision Problem
You cannot count a commodity in whole numbers. You can’t tell a machine to use “1” of resin; you tell it to use 1.5 kg.
If you use standard floating-point numbers (like JavaScript’s Number) for quantities, you will run into the classic 0.1 + 0.2 = 0.30000000000000004 problem. Over thousands of transactions, your inventory will ghost away into the ether.
The Fix:
In your database (e.g., PostgreSQL via Prisma), physical quantities must be stored as Decimal.
```prisma
model InventoryBatch {
currentQuantity Decimal @db.Decimal(18, 4) // Allows for 1.5000 kg
}
```
2. The Financial Trap: BigInt vs. Floats
Commodity prices fluctuate constantly. If your database uses floating-point math for currency (e.g., 850.25), rounding errors will compound and corrupt your financial reporting.
For an enterprise-grade system, currency must be stored as BigInt. This means you store money in its smallest unit (e.g., Cents).
->Wrong: unitPrice = 850.25 (Float)
->Right: unitPrice = 85025n (BigInt representing 850.25 Rs)
This ensures absolute mathematical precision. When you need to calculate a 15% discount or a total PO value, you do the math in integers and convert it back to a decimal string only when displaying it to the user.
3. Master Data vs. Physical Stock (The Batch System)
A beginner stores the stock quantity directly on the Material table. But a commodity is not a single monolithic block. It is composed of Batches.
You might have 1,000kg of PLA plastic, but it arrived in two shipments:
* Batch A: 400kg (Arrived Jan 1st, expires in 1 year, cost Rs. 800/kg)
* Batch B: 600kg (Arrived Mar 1st, expires in 6 months, cost Rs. 850/kg)
If you don’t track batches, you won’t know which stock is about to expire, and you won’t know the true cost of the goods you are issuing to production.
The Architecture:
Material: The master blueprint (Name, SKU, Unit, Minimum Stock).
Get Tharushi Dilara’s stories in your inbox
Join Medium for free to get updates from this writer.
InventoryBatch: The physical reality (Batch Number, Current Quantity, Expiry Date, Rack Location).
Your true stock level is always `SUM(InventoryBatch.currentQuantity)`.
4. FIFO: First-In, First-Out (For Rotating Stock)
Because commodities expire or degrade, you cannot let older batches sit on the shelf forever.
When a user requests 500kg of PLA, the system shouldn’t force them to pick a batch. The backend should automatically implement FIFO (First-In, First-Out).
1. The API fetches all AVAILABLE batches for that material.
2. It sorts them by receivedAt: ‘asc’ (oldest first).
3. It deducts from the oldest batch until the requested quantity is met. If the oldest batch only has 400kg, it deducts 400kg from Batch A and 100kg from Batch B.
5. The Two-Step Receiving Dock
In the real world, when a truck arrives with 10 tons of raw material, it doesn’t magically appear on the production floor. It sits at the Receiving Dock.
Your database must mimic this physical reality so the system doesn’t accidentally allow production to issue stock that hasn’t been verified yet.
We use a BatchStatus enum:
1. RECEIVING: The stock is logged into the system, but it is quarantined. It cannot be issued.
2. PUTAWAY: A warehouse worker scans the batch, verifies the quality, and assigns a physical location (e.g., “Rack A-03”).
3. AVAILABLE: The status is updated, and the stock is now available for production.
6. The Immutable Ledger (Audit Trail)
In commodity trading, mistakes happen. A worker might issue 10,000kg instead of 1,000kg.
Never allow deletes or edits on stock movements.If you delete a transaction, you break your audit trail and your financials will never reconcile.
Instead, implement an InventoryTransaction ledger.
* Every Stock In, Stock Out, and Adjustment writes a permanent row to this table.
* If a mistake is made, the user must create a Reversing Transaction (e.g., a Stock In of 9,000kg with the note “Correction”).
* Your UI’s “History” page simply reads this immutable ledger.
7. Partial Fulfillment
What happens if a customer or production line requests 5,000kg, but you only have 4,000kg in available stock?
A rigid system throws an error and blocks the transaction. An ERP-grade system allows Partial Fulfillment.
It issues the 4,000kg, marks the StockOut record as PARTIALLY_FULFILLED, and keeps the remaining 1,000kg as a backorder. When the next shipment arrives, the system can automatically fulfill the rest.
Summary: The Developer Mindset Shift
Building an inventory service for commodities is not about writing UI components; it’s about data integrity.
When you embrace Decimal quantities, BigInt currencies, Batch tracking, FIFO rotation, and an Immutable Ledger, you stop building a simple database wrapper and start building a system that can actually run a real world supply chain.
Are you building an ERP or inventory system? What was the hardest architectural challenge you faced? Let me know in the comments below!









