AI shopping assistant inventory can drift away from the shop’s actual stock. The assistant recommends an item from a cached catalogue, but another customer has already bought the last unit. Your bot sounds confident because the old record still looks complete.
Fetching everything live is one response, but it can make each conversation slower and put unnecessary pressure on the catalogue service. Let’s decide which facts can wait and which need a fresh check.
Give fields different freshness rules
A product description and its available quantity don’t change in the same way. You may safely display a slightly older description while requiring a current stock check before purchase.
Define freshness by operation and field. A discovery result can use a bounded catalogue snapshot. A quote needs the current payable price. An order commitment needs an inventory allocation or reservation that can reject competing buyers.
Keep source_version, updated_at and the originating service in the read model. A recent cache insertion time isn’t evidence that the source data itself was recent.
Update the read model without pretending events are instant
Catalogue changes can update a searchable read model through events. Record event versions so a delayed old update doesn’t overwrite a newer product state.
Events can be delayed, duplicated or temporarily missing. Keep a periodic reconciliation path that compares source versions and repairs gaps. A queue by itself doesn’t establish that the search index is current.
MongoDB’s modelling guidance discusses organising data around application needs. Here, the read model serves fast discovery while the source service remains responsible for current stock. Data modelling reference.
Build discovery and commitment as separate checks
The local example starts with a cached product that appears available. The current source stock then changes before checkout. The final check refuses to promise the item.
import assert from 'node:assert/strict';
let cache = { variant:'v1', version:3, stock:1, price:1000 };
let source = { variant:'v1', version:3, stock:1, price:1000 };
function apply(event) {
if(event.version <= cache.version) return false;
cache = {...event}; return true;
}
function commitRecommendation(shown) {
if(source.stock<1) return { state:'unavailable' };
if(source.price!==shown.price) return { state:'requote' };
return { state:'needs_atomic_reservation' };
}
const shown = {...cache};
source = {...source,version:4,stock:0};
assert.equal(shown.stock,1);
assert.equal(commitRecommendation(shown).state,'unavailable');
assert.equal(apply(source),true);
assert.equal(apply({variant:'v1',version:2,stock:9,price:1000}),false);
assert.equal(cache.stock,0);
console.log('PASS: stale discovery cannot confirm stock; old updates are rejected');
The recommendation wasn’t automatically a confirmed order. The commitment check catches the later change. The example also rejects an old catalogue event, which prevents one common source of cache regression.
It doesn’t reserve inventory. In a production service, combine the final stock decision with an atomic allocation rather than reading a quantity and subtracting it in a later request.
Store the recommendation the customer saw
Keep a recommendation record with product and variant IDs, displayed price, currency, catalogue version and a timestamp. This lets the next message refer to the actual result, rather than a newly generated list that happens to use the same wording.
If the price changes, explain that before asking for payment. Don’t silently send a payment request for a new amount under an old recommendation.
A saved recommendation is evidence of what was shown. It isn’t a permanent promise that inventory or pricing will never change. Your quote policy should define when an offered amount becomes binding for the order flow.
Choose a useful fallback for stale data
If the cache is stale but the source is available, refresh the facts needed for the immediate task. Avoid refetching every product attribute when only availability matters.
If the source is unavailable, distinguish browsing from buying. You may show a clearly provisional product suggestion while declining to confirm stock or take the next consequential step. Don’t invent an “in stock” answer to keep the conversation moving.
Set a deadline on live checks and return an honest next step if they fail. A customer can understand “I’m unable to confirm availability right now” more easily than an order confirmation that later disappears.
Prevent refresh traffic from becoming another outage
A popular item can trigger many simultaneous cache misses. Coalesce refreshes for the same scoped key and bound the number of concurrent source requests. Give entries staggered expiry where appropriate.
Be careful with negative caching. A briefly unavailable variant may become available again. Use a shorter policy for “not found” or “out of stock” when your business needs it, and invalidate it on relevant updates.
Keep tenant and variant identity in the key. Product names aren’t stable identifiers, and the same SKU text can exist in several shops.
Test freshness as a timeline
Warm the cache, change stock at the source and attempt checkout before the event arrives. Then deliver the update twice and deliver an older event after it. Check both the discovery view and the final order decision.
Also test a cache outage, source timeout and a price change between selection and payment. Measure stale recommendations, live-check latency and prevented commitments separately.
The fixture uses local maps, so it proves the policy rather than a distributed propagation time. To set an actual freshness target, measure event lag and source response distributions in your deployed system.
Start by separating “good enough to suggest” from “verified enough to buy”. That gives you a way to keep discovery quick without asking cached inventory to make a promise it can’t keep.
Run the example and adapt it
Save the code as 14-catalogue-freshness.mjs and run it with Node.js 24 or later:
node 14-catalogue-freshness.mjs
The code above includes assertions you can run locally. It tests the local decision logic with controlled inputs; it doesn’t connect to a live provider or establish production performance. Keep that boundary when adapting it to your application.

