Keep the in-stock filter aligned with inventory updates

Compare named inventory observations from the source system, commerce, and search to find unknown quantities, stale data, and conflicting availability.

Znode 9PatternIntermediateGCG engineering guide
In this guide
  1. Inventory source
  2. Synchronization
  3. Product projection
  4. Search index
  5. Buying experience
Trace both replenishment and depletion through every representation used to decide availability.

Available on the product page, absent from the filter

A replenished product shows stock on its detail page, but disappears when a buyer selects 'In stock' on the category page. Both screens may be reading valid data from different moments. The product page can use a fresh inventory response while search still carries an older stock projection.

Begin with a timeline rather than a cache purge. Identify when the source quantity changed, when Znode received it, when the product projection was updated, and when the search result was generated. Without that sequence, a refresh can hide the symptom while leaving the same inconsistency ready to return.

Define what in stock means

Available inventory is not always a single quantity greater than zero. Store, warehouse, reservations, safety stock, backorder policy, and product type may influence the decision. Determine which rules the existing implementation uses and which system owns each input.

Use the same business definition for the filter and the availability message, or explain their deliberate difference. An 'available to order' filter can include backorderable items, while an 'available now' filter may not. A precise label prevents a technical projection from promising a fulfillment outcome it cannot establish.

Compare scoped inventory observations

Collect StockObservation objects for the same Store and SKU at SourceSystem, Commerce, and SearchIndex. Each object carries Available and ObservedAt. In this example, Available greater than zero means in stock. Resolve warehouse, reservation, and backorder rules before mapping values when your business definition differs.

Compare returns InventoryFinding objects. Kind identifies an unknown quantity, an observation older than maximumAge, or a disagreement in availability. Stage identifies the affected source; it is null for a finding that compares several stages. Mixed Store or SKU context and future timestamps are rejected.

Inventory/InventoryProjectionCheck.cs csharp

using System;
using System.Collections.Generic;
using System.Linq;

namespace Gcg.DeveloperExamples.Inventory
{
    public enum InventoryStage
    {
        SourceSystem, Commerce, SearchIndex
    }
    public enum InventoryFindingKind
    {
        UnknownQuantity, StaleObservation, AvailabilityMismatch
    }

    public sealed class StockObservation
    {
        public InventoryStage Stage { get; set; }
        public int StoreId { get; set; }
        public string Sku { get; set; }
        public decimal? Available { get; set; }
        public DateTimeOffset ObservedAt { get; set; }
    }

    public sealed class InventoryFinding
    {
        public InventoryFindingKind Kind { get; }
        // A null Stage means the finding compares multiple stages.
        public InventoryStage? Stage { get; }

        public InventoryFinding(InventoryFindingKind kind, InventoryStage? stage)
        {
            Kind = kind;
            Stage = stage;
        }
    }

    public static class InventoryProjectionCheck
    {
        public static IReadOnlyList<InventoryFinding> Compare(
            IEnumerable<StockObservation> observations,
            int storeId, string sku, TimeSpan maximumAge, DateTimeOffset now)
        {
            if (observations == null)
                throw new ArgumentNullException(nameof(observations));
            if (storeId <= 0 || string.IsNullOrWhiteSpace(sku) || maximumAge < TimeSpan.Zero)
                throw new ArgumentException("A valid Store, SKU, and maximum age are required.");

            StockObservation[] rows = observations.ToArray();
            var findings = new List<InventoryFinding>();
            foreach (StockObservation row in rows)
            {
                if (row == null || row.StoreId != storeId || row.Sku != sku)
                    throw new InvalidOperationException("The observations contain mixed inventory context.");
                if (!Enum.IsDefined(typeof(InventoryStage), row.Stage))
                    throw new InvalidOperationException("The inventory stage is not recognized.");
                if (row.ObservedAt > now)
                    throw new InvalidOperationException("An observation cannot be in the future.");
                if (!row.Available.HasValue)
                    findings.Add(new InventoryFinding(InventoryFindingKind.UnknownQuantity, row.Stage));
                if (now - row.ObservedAt > maximumAge)
                    findings.Add(new InventoryFinding(InventoryFindingKind.StaleObservation, row.Stage));
            }

            int availabilityStates = rows.Where(row => row.Available.HasValue)
                .Select(row => row.Available.Value > 0)
                .Distinct()
                .Count();
            if (availabilityStates > 1)
                findings.Add(new InventoryFinding(InventoryFindingKind.AvailabilityMismatch, null));

            return findings.AsReadOnly();
        }
    }
}
// Map each source to a named stage. This example defines in-stock as Available > 0.
// An empty input produces no findings; callers must check that required stages were collected.

If SourceSystem and Commerce each report 12 while SearchIndex reports 0, the result includes AvailabilityMismatch with no single Stage. If the search observation is also older than the permitted age, a separate StaleObservation names SearchIndex. Before calling Compare, ensure all required stages were collected: an empty or partial list can produce no findings without proving the full inventory path is current.

Handle incomplete synchronization deliberately

A batch inventory response can contain some requested products but omit others. Decide whether omissions mean unknown, unavailable, or eligible for a bounded fallback. Do not silently convert every missing row to zero if the source contract does not define that meaning.

If fallback uses a live provider, bound its work and avoid a separate remote call for every product in a large result set. Measure the actual request path before optimizing it. A change that improves filter freshness can still create unacceptable dependency load if it replaces one projection read with hundreds of external requests.

Verify transitions and recovery

Test replenishment and depletion separately. Include two stores or warehouses when stock is scoped that way, and use quantities that make incorrect sharing obvious. Check the unfiltered listing, filtered results, product detail, and cart validation. Agreement between two pages does not prove that checkout enforces the same policy.

Pause synchronization, fail a publication step, and return an incomplete inventory response. The system should expose an operationally understandable delay or failure. After recovery, confirm that every affected projection converges without manually changing the product record. Capture each stage's source timestamp so the recovery path can be traced.

  • Distinguish missing data from known zero inventory.
  • Check cache keys for store and inventory context.
  • Record freshness lag before promising a response-time improvement.

Monitor the allowed inventory delay

Agree how long search may lag behind a source inventory change, then monitor that lag separately from API response time. A search query can finish quickly while its stock information is hours old.

Record source changes, synchronized values, and search observations on the same timeline. After a failure recovers, verify the intended products reach the current state through the normal update process. That gives the team a specific synchronization step to fix instead of a recurring instruction to clear caches.

References and further reading

Bring your next engineering question.

Connect catalog and inventory workflows

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.

Previous
Previous

Build a Znode 10 Page Builder widget for preview and storefront