</>Oriol Martí
← Blog

Real-time stock across e-commerce, ERP and physical stores

· 5 min read

Selling the same product through three channels at once looks like a business problem. It’s actually a distributed systems problem: three systems, each with its own idea of how many units are left, and a customer who doesn’t want to hear “sorry, we didn’t have it after all”.

This is the case study of how we synced stock in real time between an online store, an ERP and a retailer’s physical shops, as the tech team responsible for all three pieces.

Context

The business sold through three channels:

Channel System Who controlled it
Online store E-commerce Us
Management, purchasing and warehouse ERP An external vendor
Physical shops POS (point of sale) Us: we built it from scratch

We built the POS from the ground up with Vue.js and PHP, designed for the business’s own shops. That gave us a real advantage: two of the three systems were ours, and we could decide how they talked to each other.

The third, the ERP, wasn’t. It had its own data model, its own timing and its own rules. Like almost every ERP, it wasn’t designed with real time in mind.

The real problem: who’s in charge?

Before talking about queues or APIs, one question decides everything: which system is the source of truth for stock?

If every system believes it is, the outcome is predictable:

  • An online sale decrements stock in the e-commerce, but the ERP only finds out later.
  • Meanwhile, a physical shop sells the last unit of the same product.
  • A restock arrives at the warehouse and the ERP records it, but the online store still says “sold out”.

Every system is right given the information it has. The problem is that none of them has all of it.

There are several ways to solve this, each with its costs:

Approach Upside Cost
The ERP is the only truth and everyone asks it One place to look Every sale depends on the ERP being fast and available
Each channel keeps its own stock, reconciled periodically Independent channels Lag windows where you can sell what isn’t there
The ERP is the truth, but every movement propagates as an event Near real time without coupling each sale to the ERP More moving parts: queues, retries, idempotency

Design

The key idea is to stop thinking of “stock” as a number copied from one place to another, and start thinking in stock movements: an online sale, an in-store sale, a return, an incoming delivery. Each movement is a fact that happens in one system and that the others need to know about.

  Online sale ──┐                      ┌─▶ E-commerce (visible stock)
  POS sale ─────┼─▶ stock movement ────┼─▶ ERP (accounting, purchasing)
  Restock ──────┘   (queue + retries)  └─▶ POS in each shop

Movements, not numbers

Propagating “3 units left” is fragile: if two messages arrive out of order, the final number is wrong. Propagating “1 unit sold” is more robust, as long as each movement is applied exactly once. So every movement needs a unique identifier, and whoever receives it must be able to ignore duplicates.

The ERP, behind a queue

The ERP can’t sit on the critical path of a sale. If it’s slow or down, the online store and the POS still have to sell. So communication with the ERP is decoupled: movements are queued and sent with retries. If the ERP goes down, movements wait; when it comes back, they’re processed in order.

Conflicts and overselling

With three channels selling in parallel, overselling can’t be eliminated entirely; it can only be made unlikely, with a plan for when it happens anyway. The usual levers are:

  • Shrinking the lag window, propagating movements as soon as they happen.
  • Safety buffers on low-stock products, showing “sold out” online slightly earlier.
  • Reservations: decrementing stock when checkout starts, not when it ends.
  • A clear process for when something is sold that isn’t there.

Catalogue tooling

Syncing stock only works if all three systems are talking about the same product. Part of the work, less glamorous, was building tools in PHP and Node.js to create and maintain the catalogue, both online and offline. If product references don’t match across systems, no event architecture will fix it.

Outcome

The result was real-time stock across the online store, the ERP and the physical shops, with an in-house POS that was part of the same architecture rather than an island.

What I’d change today

The architecture solved the problems we had. With what I know now, I’d change a few things:

  • A movement log as the internal source of truth, with each channel’s stock as a projection of it. That makes it easy to audit what happened and to rebuild state if something gets corrupted.
  • More visibility into lag: knowing at any moment how far behind each system is, and alerting when the ERP queue grows.
  • Automatic periodic reconciliation between systems, to catch differences before a customer does.

What I learned

The important question isn’t technical. “Who’s the source of truth?” is answered by the business: who buys, who sells and who’s accountable when the numbers don’t match. The architecture comes after.

Propagate facts, not state. Movements can be ordered, deduplicated and replayed. Numbers copied from one place to another can’t.

Decouple what you don’t control. The ERP was the most important system and the one we controlled least. Putting it behind a queue meant its problems didn’t become the store’s problems.

Building the POS was an architecture decision. Owning the point of sale wasn’t just about features: it let the third channel speak the same language as the other two.

Syncing stock looks like just another integration. It’s actually one of the most real consistency problems in e-commerce, because when it goes wrong, a customer notices.