php 116 lines · 3 tabs

Warm a Related-Products Cache with a Queued Laravel Job on Inventory Change

Shared by codesnips Sep 2026
3 tabs
<?php

namespace App\Observers;

use App\Models\Product;
use App\Jobs\WarmRelatedProductsCache;

class ProductObserver
{
    public function saved(Product $product): void
    {
        if (! $product->wasChanged(['stock', 'is_active'])) {
            return;
        }

        WarmRelatedProductsCache::dispatch($product->id)
            ->delay(now()->addSeconds(15))
            ->onQueue('cache-warming');
    }

    public function deleted(Product $product): void
    {
        // Neighbours of the same category may now be stale.
        Product::where('category_id', $product->category_id)
            ->pluck('id')
            ->each(function ($id) {
                WarmRelatedProductsCache::dispatch($id)->onQueue('cache-warming');
            });
    }
}
3 files · php Explain with highlit

This snippet shows how an e-commerce catalog keeps its "related products" cache warm without blocking the request that changed inventory. The pattern is read-through caching combined with proactive warming: instead of recomputing the related-products list lazily on the next page view (which risks a slow first hit or a thundering herd after a bulk stock update), the recompute is pushed onto a queue and executed in the background.

In ProductObserver, Eloquent's model events act as the trigger. The observer's saved method inspects the dirty attributes via wasChanged and only reacts when stock or is_active actually moved, avoiding needless work when unrelated fields change. It then dispatches WarmRelatedProductsCache and applies a ->delay(...) to debounce rapid successive edits, so a burst of stock adjustments coalesces rather than firing a job per keystroke.

The queued class WarmRelatedProductsCache carries only the product id, not the whole model, which keeps the serialized payload small and guarantees the job reads fresh data at execution time. It implements ShouldQueue and sets $uniqueFor with ShouldBeUnique so that overlapping jobs for the same product collapse into one — the key defence against redundant recomputation. uniqueId scopes the lock to the product, and handle re-fetches the product, bails out gracefully if it was deleted, computes the neighbour set, and writes it under a stable cache key with a TTL that outlives the natural page cache.

The heavy lifting lives in RelatedProductsService, which is where the domain logic sits: it queries same-category, in-stock products, scores them, and returns a lean array of ids and display fields. Storing a plain array rather than full models keeps the cached value compact and framework-version tolerant. The relatedFor read path uses Cache::remember, so if the warm job has not yet run the value is still computed on demand — the warm job simply ensures that cold reads are rare. Together these files illustrate the trade-off of eventual consistency: the cache may briefly lag a stock change, which is acceptable for a recommendations widget, in exchange for fast, non-blocking writes and bounded recompute cost.


Related snips

Share this code

Here's the card — post it anywhere.

Warm a Related-Products Cache with a Queued Laravel Job on Inventory Change — share card
Link copied