When to use this
- You run a multi-tenant app against a shared cache and need writes to invalidate precisely without leaking across tenants.
- You're seeing stale reads after an update, or the classic “why am I seeing another client's data?” cache-key collision.
- You're introducing tagged caching and want the invalidation discipline defined before it ships.
Prerequisites
- A tag-capable cache store (Redis or Memcached via Laravel's cache tags) — the database and file drivers do not support tags.
- Tenant context resolvable at cache-access time (the same context your queries use).
- The current deploy SHA or schema version available at runtime (for key versioning).
Procedure
- 1
Prefix every key with the tenant
The single most common multi-tenant cache bug is a global key — `user-roles`, `settings` — warmed by whichever tenant hit it first and then served to everyone. Route all cache access through a wrapper that prefixes the active tenant so a collision is structurally impossible.
A tenant-scoped cache wrapper final class TenantCache { public function remember(string $key, int $ttl, Closure $cb): mixed { $tenant = app(TenantContext::class)->requireId(); // Every key is physically namespaced by tenant — no cross-tenant hit. return Cache::remember("t:{$tenant}:{$key}", $ttl, $cb); } } - 2
Group related keys with tags
Tags let one write invalidate exactly the keys it affects, instead of flushing the whole store or trying to enumerate keys by hand. Tag by tenant and by the entity the cache derives from, so a change to one order clears that order's caches and nothing else.
Tag on write, flush on change $tenant = app(TenantContext::class)->requireId(); Cache::tags(["t:{$tenant}", "order:{$orderId}"]) ->remember("order-summary:{$orderId}", 600, fn () => $this->build($orderId)); // On update, flush precisely — only this order's tagged entries. Cache::tags(["order:{$orderId}"])->flush(); - 3
Version keys by schema and deploy
When a deploy changes the shape of a cached value, old entries must not be read by new code. Fold a schema version and the deploy SHA into the key so a changed contract simply misses and rebuilds, rather than deserialising into the wrong shape.
Version prefix — a changed shape misses cleanly $v = config('cache.schema_version').':'.config('app.deploy_sha'); Cache::remember("v{$v}:t:{$tenant}:{$key}", $ttl, $cb); - 4
Invalidate before you warm — and treat a partial warm as an alarm
When a change triggers a rebuild, invalidate the stale entries first, then prefill. Reversed, the prefill builds values the invalidation immediately deletes. A prefill that only partially completes must page — a half-warm cache serves a view assembled from fresh and stale values at once.
Ordered invalidate → warm, with a partial-warm guard Cache::tags(["order:{$orderId}"])->flush(); // 1. invalidate first $result = $this->warm($orderId); // 2. then prefill if ($result->failedKeys) { // A partial warm is not a warning to log — it is a page to answer. Alert::page('cache prefill partial', ['order' => $orderId, 'failed' => $result->failedKeys]); } - 5
Always keep a TTL as the backstop
Tags give you precision; a TTL gives you safety. Never cache with `forever`/`rememberForever` on data that can change — if an invalidation is ever missed, the TTL guarantees the wrong value self-heals instead of persisting indefinitely. Precision from tags, correctness insurance from the TTL.
Verify it worked
- Two tenants requesting the same logical key receive different values — assert it in a feature test, once.
- After an update, the tagged flush clears exactly the affected keys and leaves unrelated tenants' entries intact.
- A deploy that changes a cached value's shape misses the old entries (the version prefix changed).
- No production cache path uses `forever`/`rememberForever` for mutable data — grep the codebase and confirm.
If it goes wrong — rollback
- Disable the tagging path behind a flag and fall back to TTL-only caching — less precise, still correct.
- If invalidation logic is suspect, flush the affected tenant's namespace wholesale and let entries rebuild on demand.