- A supplier imports a new inventory feed.
- Or the opposite happens: a product has been physically unavailable for weeks, yet buyers continue discovering it as if it could be ordered.
- Both symptoms expose the same architectural problem.
- For a supplier-driven procurement marketplace, we built a durable stock-visibility lifecycle around Medusa and Elasticsearch.
The client problem
A supplier imports a new inventory feed. The Medusa stock level is positive again, but the product remains absent from marketplace search.
Or the opposite happens: a product has been physically unavailable for weeks, yet buyers continue discovering it as if it could be ordered.
Both symptoms expose the same architectural problem. Inventory truth lives in the commerce engine. Discovery lives in a search projection. An event normally connects them—but events can arrive late, fail after the database commit or be missed during an interrupted process.
For a supplier-driven procurement marketplace, we built a durable stock-visibility lifecycle around Medusa and Elasticsearch. It records the commercial visibility decision independently from product publication, persists pending search actions and regularly recomputes them from canonical stock.
The result is not a promise that synchronisation never fails. It is a repairable system in which search can converge again without replaying an entire supplier feed.
Inventory and discoverability are different questions
Medusa answers how much inventory exists and whether a variant can be fulfilled. The catalogue also needs a policy for what buyers should continue to discover.
A temporary stockout should not necessarily remove a product immediately. Suppliers may replenish it tomorrow, and disappearing product pages can damage comparison and purchasing continuity. A product that has remained unavailable beyond the marketplace's tolerance may no longer belong in search results.
We therefore keep marketplace stock suspension separate from Medusa's editorial publication status.
A product can remain published in Medusa while carrying a reversible search-visibility state. The state records when it first became physically unavailable, when it was suspended and which Elasticsearch action still needs to happen.
This distinction preserves ownership. Product publication remains an editorial or supplier decision. Stock suspension is an operational discovery rule. Restocking can reverse the latter without republishing or reconstructing the product.
Define what “available” means for the catalogue
Stock is not always one number.
A variant may not manage inventory. It may allow backorders. It may depend on several inventory components, each with a required quantity. Reservations can reduce what is available to new carts without changing how much physical stock the supplier owns.
The project's marketplace rule evaluates its active variant. Unmanaged and backorderable products remain available. For managed inventory, component stock is aggregated across locations and kit availability follows the least fulfilable component quantity.
The long-term out-of-stock clock uses physical stocked_quantity, not a reservation-reduced availability value. A buyer reserving the last unit should not make the marketplace believe the supplier has been physically empty for thirty days.
This is a business definition encoded once. Import subscribers, item-change handlers, scheduled repair and search indexing all use the same observation rather than inventing availability independently.
Give temporary stockouts a configurable grace period
When a published product first becomes unavailable, the state records out_of_stock_since.
Before the configured threshold is reached, the product is not suspended. Search can receive a refresh because the stock facts changed, but the listing remains discoverable under the marketplace policy.
Once the elapsed time crosses the threshold, the state records a suspension and queues a hide action for Elasticsearch. If physical stock returns, the clock and suspension are cleared and a show action is queued. If an unavailable product had not yet been suspended, restocking simply clears the clock and refreshes the document.
The threshold is persisted as an operational setting with bounded validation. The default in this project is thirty days, but the architecture does not bury that policy in a cron expression or frontend condition.
The marketplace can decide how patient discovery should be while keeping the transition deterministic.
Persist the decision before touching Elasticsearch
An inventory update and a search request do not share one database transaction.
Trying to pretend they do creates fragile behaviour. If Elasticsearch fails after Medusa commits the stock change, rolling back the commerce mutation is neither possible nor desirable. If the search call happens first, buyers may see a projection for a state that never became canonical.
Our change handler evaluates the affected products and persists the next stock-visibility state first. It then attempts the pending Elasticsearch action.
If search synchronisation fails, the inventory or product workflow is not failed retrospectively. The state keeps pending_es_action and a bounded error for later repair.
This turns a transient integration failure into durable work. The database can answer which products still need to be hidden, shown or refreshed. A scheduled reconciler can drain that queue without guessing from logs.
Canonical commerce truth leads; search follows.
Protect against an old observation winning a race
Stock can change while an evaluator waits.
Imagine an out-of-stock event begins evaluation, then a replenishment import commits before the first evaluator acquires its state lock. Applying the earlier observation afterwards could restart the clock or hide a product that is already back in stock.
The evaluator creates and locks the relevant marketplace stock-state rows in stable product order. After obtaining those locks, it reads the current canonical product and inventory observation again inside the transaction.
The transition is planned from that fresh observation, not from the stale event payload that merely selected the product IDs.
This makes events hints about what to reconsider rather than authoritative snapshots. The source of truth remains the latest Medusa and inventory state at the protected decision boundary.
That pattern is central to reliable reconciliation: use notifications to narrow the work, then recalculate the decision from canonical data.
Keep hide, show and refresh as durable actions
Elasticsearch needs three different behaviours.
hide removes a suspended product from discovery. show rebuilds and indexes a product whose suspension was lifted. refresh updates a still-visible product whose stock facts changed.
The pending action lives on the product's stock state until the corresponding search operation succeeds. Synchronisation processes products in bounded batches and acquires the same per-product search-write locks used by other indexing paths.
After acquiring the lock, it re-reads the current pending action and rebuilds show documents from current canonical data. If another evaluator changed hide into show while an earlier request was in flight, compare-and-set updates preserve the newer action rather than clearing it accidentally.
Failed product IDs keep their action and error. Successful rows clear the exact action they completed.
The queue describes intent, and completion acknowledges only the intent that was actually applied.
Reconcile more than failed search requests
A durable pending queue repairs known Elasticsearch failures. It does not find every way state can drift.
An event may never have run. A deployment may interrupt an import after stock commits but before its subscriber resolves product IDs. A product may cross the grace-period threshold simply because time passed, with no new inventory event to trigger evaluation.
The scheduled reconciler covers those gaps through several bounded scans:
- a resumable one-time bootstrap visits products without requiring one enormous run;
- an overlapping inventory-change scan revisits recently updated levels;
- a due scan finds unavailable products whose threshold has now elapsed;
- a suspended-product audit checks whether visible truth changed;
- the pending Elasticsearch queue retries unapplied actions.
This is why reconciliation cannot be reduced to “retry the last event”. It recomputes the world that should exist now.
Make the repair job resumable and single-owned
A global repair process should not compete with itself.
The reconciler uses a lock so another instance skips when one run already owns the work. Its job-state record keeps the bootstrap cursor, completion time, latest inventory scan and suspended-audit cursor.
Work is paged in batches rather than loading every product into memory. Bootstrap is deliberately capped per run and resumes later. The inventory scan overlaps its previous boundary to reduce the chance that an update near the cutoff is missed; evaluating the same product again is safe because the decision is recalculated.
This design makes the daily safety net predictable. It can advance through a large catalogue over several executions and recover after interruption from durable cursors.
The target is convergence, not one heroic full-table job that must finish perfectly every night.
Recover after an interrupted inventory import
Suppose an inventory import applied several chunks and then stopped.
Earlier stock changes remain valid because the import commits per chunk. Some affected-product notifications may have run and others may not. Replaying the full feed blindly—especially in additive mode—may not be the safe response.
The marketplace visibility repair does not need to replay inventory mutations. Its changed-inventory scan and canonical evaluator can find products whose physical levels were updated, recompute their current availability and queue the correct search action.
If the import itself needs recovery, its staging records decide which inventory rows remain. If discovery needs recovery, the stock-visibility reconciler derives the projection from the inventory state that actually committed.
Separating those two responsibilities prevents a search inconsistency from forcing a risky business-data replay.
A practical stock-visibility checklist
Before hiding out-of-stock products from Medusa search, ask:
- Is product publication separate from stock suspension?
- What exact stock observation defines availability?
- Are backorders, unmanaged inventory and kits handled explicitly?
- Does the long-term clock use physical stock rather than reservations?
- Is the grace period configurable and persisted?
- Is the next visibility state committed before search is called?
- Does a search failure leave a durable pending action?
- Are observations re-read after locking state?
- Can concurrent hide/show changes preserve the newer intent?
- Does reconciliation scan changed, due and suspended products?
- Is bootstrap resumable and bounded?
- Do every indexing and reindexing path share the suspension predicate?
- Can discovery repair run without replaying inventory mutations?
If search can only recover by receiving every event exactly once, it is not a repairable projection.
Why Medusa was the right foundation
Medusa remained the source for products, variants and inventory. We extended it with a marketplace-specific visibility policy, durable transition state and an Elasticsearch convergence loop.
That let the client express a subtle commercial rule: temporary stockouts remain discoverable, prolonged physical unavailability suspends discovery, and restocking restores the product automatically.
More importantly, the capability survives imperfect integrations. Events make updates fast. Reconciliation makes the result dependable.
When stock returns, the marketplace should not need an operator to republish the product or re-upload the supplier file. The system should recalculate what buyers are allowed to discover and move search toward that truth.
