Cache invalidation and cache stampede in .NET: HybridCache tags, TTLs and what not to cache

Cache-aside: read from the cache, otherwise from the database, and write to the cache.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ======================================================================
// Q: How do you handle cache invalidation and cache stampede?
//
// - Cache-aside: read from the cache, otherwise from the database, and write to
//   the cache. Always with a TTL, even when there is explicit invalidation, so
//   that a mistake fixes itself.
// - Invalidation on change: when a value changes (e.g. a PriceListUpdated event
//   from the ERP), delete the related keys or tags.
// - Stampede: a popular key expires and 500 requests hit the database at once.
//   HybridCache solves this on its own (only one factory call per key).
//   IMemoryCache.GetOrCreateAsync does not. Random jitter on the TTL also helps.
// - Multiple instances with IMemoryCache: each one holds a different version of
//   the data. Solution: Redis, or HybridCache with a short L1 TTL.
// What I do NOT cache, or cache very carefully:
// - Stock at checkout: a stale value means overselling. In the product list an
//   approximate value is enough, but at checkout you check the database (Q3).
// - Per-customer or per-tenant data without the customerId / tenantId in the
//   key: data leaks from one customer to another (Q36).
// - The cache is an optimization, not the source of truth. If Redis goes down,
//   the application keeps working (more slowly) from the database.
// ======================================================================

// Price list change from the ERP: invalidation by tag
public class PriceListUpdatedHandler(HybridCache cache, IOutputCacheStore outputCache)
    : IDomainEventHandler<PriceListUpdated>
{
    public async Task Handle(PriceListUpdated e, CancellationToken ct)
    {
        await cache.RemoveByTagAsync($"prices:{e.PriceListId}", ct);
        await outputCache.EvictByTagAsync("catalog", ct);
    }
}