Custom Inventory Management System Development: Build, Extend, or Buy?
Custom inventory management system development explained: build vs buy, version-one features, ledger data model, oversell prevention and cost drivers.
Long Nguyen
Développeur fullstack · Ingénieur IA · Chercheur
What does custom inventory management system development involve?
Custom inventory management system development is the work of building stock software around the way your goods actually move: your locations, your units of measure, your sales channels and your accounting rules. A packaged product asks you to adapt to its model. A custom build starts from yours.
Whatever the industry, a working system has four parts:
- A movement record: every receipt, sale, transfer, return and adjustment, stored as its own row.
- Balances: the current quantity per item, location and state, calculated from that record.
- Integrations: the code that sends quantities to each sales channel and brings orders back.
- Screens and scanners: what your staff use to receive, pick, count and correct.
Buyers usually arrive with a list of screens. Most of the engineering effort goes into the movement record and the integrations, and those two decide whether the numbers can still be trusted a year in. This guide covers when a custom build makes sense, what version one should contain, how the data should be modelled, how overselling is prevented, and what moves the price.
Build, extend, or buy: which route fits your business?
There are three routes, and the right one depends on how far your process sits from the standard one.
| Buy a packaged app | Extend an ERP | Build custom | |
|---|---|---|---|
| Fits when | Your process matches the product's model and your channels have supported connectors | Your process is mostly standard, with a few rules of your own | Your stock rules are unusual, or they are what you compete on |
| You give up | Control over the data model and the sync logic | Some freedom: you work inside the ERP's concepts | Speed to first use; nothing exists until it is built |
| Ongoing cost | Subscription, often scaling with users, orders or locations | Hosting plus maintenance of your custom modules | Hosting plus maintenance of everything |
| Main risk | Outgrowing it and migrating later | Customising so heavily that upgrades become hard | Scope creep and under-specified integrations |
A custom build is usually justified when at least one of these is true:
- Your units don't fit a standard catalog: fabric sold by the metre from rolls, kits assembled to order, serialised parts, consignment stock you hold but don't own.
- You sell on a channel that packaged tools don't connect to, or their connector syncs too slowly for your order volume.
- Allocation rules are part of how you compete, such as holding stock back for wholesale accounts or setting a different buffer per channel.
- Per-user or per-order fees are growing faster than the business.
It is usually not justified for a short catalog in one location selling through one channel. The inventory built into your store platform, or a well-kept spreadsheet, does that job.
The middle route deserves more attention than it gets. An open-source ERP already has receipts, transfers, reordering and valuation, so the custom work shrinks to the rules that are yours alone. If your process is mostly standard, Odoo development with custom modules will usually reach a working system sooner than an empty repository will.
Which features belong in version one?
Version one should be the smallest system your team can run the business on. One rule sorts the list: anything that changes the shape of the movement record gets decided now, even if its screens ship later.
| Capability | When | Reason |
|---|---|---|
| Item and variant catalog with units of measure | Version one | Everything else refers to it |
| Movement record and balances per location | Version one | The core of the system |
| Receiving against purchase orders | Version one | Stock has to enter with a cost and a source |
| Order reservation and fulfilment | Version one | This is where overselling is prevented |
| Sync with your highest-volume sales channel | Version one | Proves the integration pattern on real orders |
| Adjustments with reason codes, and cycle counts | Version one | Reality and the system will disagree; you need a clean way to correct it |
| Low-stock alerts and reorder points | Version one | Cheap once balances exist |
| Lot, serial or expiry tracking | Decide now | It adds a dimension to every movement and is expensive to retrofit |
| Costing method | Decide now | It determines what each receipt has to store |
| Additional channels and warehouses | Later, one at a time | Each one is its own integration and test cycle |
| Barcode scanning and mobile apps | Later, unless you already scan | An input method on top of the same movements |
| Demand forecasting | Later | It needs months of clean movement history to work from |
How should the inventory data model be designed?
The most common design mistake is a quantity column on the product table that gets overwritten on every sale. It works until someone asks why the system says 37 and the shelf says 34, and there is no history to answer with.
The design that holds up is a ledger. Every change in stock is appended as a movement row with a signed quantity, a reason, and a reference to the document that caused it. Nothing is edited or deleted; a mistake is corrected with another movement. Balances are a running total kept beside the ledger for fast reads, and they can always be rebuilt from it.
Stock is several numbers, not one
'How many do we have?' has more than one answer. Units on the shelf, units you can sell, and units already promised to a customer are three different quantities. Shopify's inventory states documentation is a useful reference model because it names eight of them:
| State | Meaning |
|---|---|
incoming | On its way to a location, not yet physically there |
on_hand | Total units physically at a location |
available | Units that can be sold |
committed | Units set aside, for example for unfulfilled orders |
reserved | On-hand units temporarily set aside |
damaged | On-hand units that can't be sold or used |
safety_stock | On-hand units held back to guard against overselling |
quality_control | On-hand units currently in inspection |
In that model, on hand equals the sum of available, committed, reserved, damaged, safety stock and quality control. Incoming sits outside it. A first version rarely needs all eight, but it does need to keep physical stock and sellable stock apart from day one. Systems that merge them are the ones that oversell.
A ledger schema to start from
Two PostgreSQL tables carry the design:
CREATE TABLE stock_movement (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku_id bigint NOT NULL,
location_id bigint NOT NULL,
state text NOT NULL,
quantity integer NOT NULL CHECK (quantity <> 0),
reason text NOT NULL,
reference text NOT NULL,
idempotency_key text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE stock_balance (
sku_id bigint NOT NULL,
location_id bigint NOT NULL,
state text NOT NULL,
quantity integer NOT NULL CHECK (quantity >= 0),
PRIMARY KEY (sku_id, location_id, state)
);The reference column ties each movement to its source: a purchase order, a sales order, a count sheet. Shopify's inventory mutations accept a reference document URI for the same audit-trail purpose. The CHECK (quantity >= 0) on balances is a last line of defence: if application code ever tries to sell stock that isn't there, the database refuses.
How do you prevent overselling across sales channels?
Overselling has three separate causes, and each needs its own fix.
| Cause | What it looks like | Fix |
|---|---|---|
| Two orders race for the last unit | Both succeed and stock goes negative | Check and deduct in one guarded database statement |
| The same event arrives twice | An order is deducted twice after a webhook retry | A unique idempotency key per event |
| A channel shows an old quantity | A marketplace sells a unit that sold elsewhere minutes earlier | Push on every change, reconcile on a schedule, hold a buffer on slow channels |
Reserve stock in one guarded transaction
The first two causes are solved inside your own database. This transaction reserves two units of item 42 for an incoming order:
BEGIN;
INSERT INTO stock_movement
(sku_id, location_id, state, quantity, reason, reference, idempotency_key)
VALUES
(42, 1, 'available', -2, 'order_reserved', 'shopify:order:1001', 'shopify:1001:line1:out'),
(42, 1, 'reserved', 2, 'order_reserved', 'shopify:order:1001', 'shopify:1001:line1:in');
UPDATE stock_balance
SET quantity = quantity - 2
WHERE sku_id = 42 AND location_id = 1 AND state = 'available'
AND quantity >= 2;
-- 0 rows updated means not enough stock: ROLLBACK and reject the line
INSERT INTO stock_balance (sku_id, location_id, state, quantity)
VALUES (42, 1, 'reserved', 2)
ON CONFLICT (sku_id, location_id, state)
DO UPDATE SET quantity = stock_balance.quantity + EXCLUDED.quantity;
COMMIT;If the UPDATE reports zero rows, there was not enough stock, and the order line is rejected before anything is committed. If the same webhook is delivered again, the first INSERT fails on the unique key and the whole transaction rolls back, so nothing is counted twice. Both behaviours were tested on PostgreSQL 16.
Make one system the source of truth
The third cause is an integration problem. Your inventory system should own the available number. Channels receive it, and orders come back as reservations. If staff can also edit quantities by hand inside a marketplace dashboard, the two sides drift, and sync code cannot repair that.
Writes to a channel need the same care as writes to your own database. Shopify's inventorySetQuantities mutation takes a compareQuantity value and only applies the change if the stored quantity still matches it. From API version 2026-04 it also requires an idempotency key on every call. Where a channel's API offers neither protection, a scheduled full reconciliation has to catch what the event-driven pushes miss, and it belongs in the design either way.
Which costing method should the system support?
Costing decides what each unit on the shelf is worth in your accounts, and it shapes the data model as much as lot tracking does. Under IAS 2 Inventories, the international accounting standard, inventories are measured at the lower of cost and net realisable value. Cost is assigned by specific identification for items that are not ordinarily interchangeable, and by first-in, first-out (FIFO) or weighted average for items that are.
| Method | What the system must store | Suits |
|---|---|---|
| Specific identification | The actual cost of each individual unit, tied to its serial number | One-off or serialised goods such as vehicles, machinery or artwork |
| FIFO | Cost layers: each receipt keeps its unit cost and the quantity still unsold | Goods with changing purchase prices, and perishables, where it mirrors the physical flow |
| Weighted average | A running average cost per item, recalculated on every receipt | Large volumes of identical items where layer tracking adds little |
LIFO is not among the formulas IAS 2 offers. US tax rules still accept it, so a company that reports only in the United States may ask for it. Confirm the method with your accountant before development starts, because switching later means recalculating the cost of every historical movement.
One trap to settle in the specification is the backdated receipt. If a supplier invoice is entered after goods from that delivery have already been sold, FIFO layers and running averages both have to be recalculated from that date forward. Decide whether the system recalculates automatically or blocks entries dated inside a closed accounting period.
What drives the cost and timeline of a custom build?
No one can price an inventory system from its name. The estimate comes from a short list of drivers, and you can read your own project off this table before talking to any developer.
| Driver | Effect on effort | Why |
|---|---|---|
| Number of sales channels to sync | High | Each has its own API, authentication, rate limits, error formats and sometimes an app approval process |
| Lot, serial or expiry tracking | High | Adds a dimension to every movement, every screen and every report |
| Kits, bundles and unit conversions | Medium to high | One sale has to deduct several components, or a fraction of a larger unit |
| Data migration | Medium, often underestimated | Duplicate SKUs, missing units and inconsistent names have to be cleaned before import |
| Number of locations and transfers | Medium | Stock in transit is its own state with its own rules |
| Barcode hardware and mobile screens | Medium | Device testing, offline behaviour and label printing |
| Costing and valuation reports | Medium | The figures must agree with your accounting system |
| User roles and permissions | Low | Standard work unless approval chains are involved |
Two points protect your budget. First, ask for the estimate split by phase, with version one priced on a fixed scope. A fixed price for the whole system, quoted before anyone has looked at your item list and your channels, is a guess. Second, budget for maintenance from the start. Sales platforms revise their APIs on a schedule; Shopify releases a new API version every quarter and retires old ones, so integration code needs regular attention after launch.
What does the development process look like, step by step?
- Map the movements. Follow one unit from purchase order to customer delivery and back through a return. Every change of place or status is a movement type the system must record.
- Clean the catalog. Unique SKUs, agreed units of measure and working barcodes come before any code. Software cannot fix an item that exists under three names.
- Build the ledger and balances. Include automated tests that fire concurrent orders at the last unit, because that is the failure you most need to rule out.
- Add receiving, adjustments and counts. At this point the system can already replace a stock spreadsheet.
- Connect the first channel in shadow mode. The system reads orders and calculates quantities but does not push them. Compare its numbers with reality until they agree.
- Go live from a physical count. The count becomes the opening movements, and pushing quantities to the channel is switched on.
- Add channels and locations one at a time. Each gets its own shadow period before it is trusted.
Shadow mode is the step most often skipped under deadline pressure, and it is the cheapest place to find sync bugs, because nothing a customer can see depends on the new system yet.
What should you prepare before asking for a quote?
A developer can give a realistic estimate once these six things are on the table:
- An export of your item list, with variants and units of measure.
- Your stock locations, including any third-party warehouse.
- Every sales channel you use and its rough monthly order volume.
- Stock rules that are specific to you: lots, serials, kits, consignment, per-channel buffers.
- The costing method your accountant wants.
- What your current tool or spreadsheet gets wrong, with a recent example.
The last item matters most. A concrete failure, such as an order that oversold last month, tells a developer more about the system you need than a feature list does.
Netalith builds inventory and multichannel systems as custom software and SaaS projects, priced to scope. Send the list above and you will get a phased estimate for your catalog and channels.
FAQ
Questions fréquentes
What is the difference between an inventory management system and a warehouse management system?
An inventory management system tracks how much stock you own, where it is, what it cost and what is available to sell. A warehouse management system directs the physical work inside a building: bin locations, pick paths, wave picking and labour. Most small and mid-sized sellers need the first and only add warehouse features once a single site becomes hard to run by eye.
Can a custom inventory system work with my existing Shopify, Amazon or eBay store?
Yes. The custom system connects to each store through its API, receives orders as they are placed and sends updated quantities back. Your storefronts and listings stay where they are. The usual design makes the custom system the single source of truth for available stock, with each channel receiving that number rather than keeping its own.
Is Odoo a good base for a custom inventory management system?
It is when your process is mostly standard. Odoo already covers receipts, transfers, reordering rules, lots and valuation, so custom modules only need to add the rules specific to your business. A from-scratch build suits cases where the stock logic itself is unusual, or where the system is a product you plan to sell to others.
Do I need barcode scanning in the first version?
Only if your team already scans today. Scanning is an input method on top of the same stock movements, so it can be added later without changing the data model. What should be in place from the start is a unique, clean identifier for every item, because scanners depend on it.
How do I move from spreadsheets to a custom inventory system without losing data?
Clean the item list first: one row per SKU, agreed units, no duplicates. Import it as the catalog, then take a physical count and load the result as opening movements, so the system starts from verified quantities instead of spreadsheet figures. Keep the spreadsheet read-only for a period as a reference, and do not run both as live records.
Who owns the code and data in a custom inventory system?
That depends on the contract, so settle it in writing before work starts. A typical arrangement gives the client ownership of the source code and all data once the project is paid for, with hosting accounts registered in the client's name. Ask for repository access during the build instead of a handover at the end.