Operations

Caching Strategy

Intermediate

Caching makes things fast by keeping a copy of data closer to where it is needed. It is also where two of the worst kinds of bug live: stale data and, worse for us, one tenant seeing another's data through a shared cache key. Cache on purpose, key correctly (including the tenant), and always know how the cache gets invalidated.

A cache trades freshness for speed. That trade is often worth it, but it is still a trade. So every cache needs three decisions made on purpose: what to cache, how it is keyed, and when it expires or is invalidated. There is an old saying that the two hardest things in computer science are cache invalidation and naming things, and it is largely true.

For a multi-tenant platform the cache key is a security concern, not just a correctness one. A key that leaves out the tenant can serve one customer's data to another straight from memory, bypassing every database check (see Multi-Tenancy). Get the key right first, then worry about speed.

Cache correctly and safely

Tenant-blind key var key = $"customer:{id}";
return cache.GetOrAdd(key, () => Load(id)); // no tenant in key

If ids are not globally unique across tenants, or simply collide, one tenant can be served another's cached customer, bypassing the database's tenant checks entirely. That is a silent cross-tenant data leak.

Tenant in the key, bounded TTL var key = $"{ctx.TenantId}:customer:{id}";
return cache.GetOrAdd(key, TimeSpan.FromMinutes(5),
() => Load(ctx.TenantId, id));

The tenant is part of the key, the entry expires, and the loader is itself tenant-scoped. Fast and isolated.

Keep it from causing bugs

Self-review checklist

Why it matters: Caching is one of the highest-value performance tools and one of the easiest ways to cause a serious bug: serving stale data, or worse, leaking one tenant's data to another. Correct, tenant-aware keys, deliberate invalidation, and treating the cache as an optimisation over a correct system keep the speed without the danger.