I'm working on a shared-schema multi-tenant application (ASP.NET Core 8, EF Core, PostgreSQL) where every table carries a TenantId column and a global query filter scopes reads. I'm now adding Redis caching via StackExchange.Redis and I'm unsure how to scope cache entries so one tenant can never read another's data.
I've seen three approaches suggested and I can't find a clear discussion of the trade-offs.
1. Key prefixing on a shared database
csharp
public class TenantCacheKeyBuilder
{
private readonly ITenantContext _tenant;
public string Build(string key) => $"t:{_tenant.TenantId}:{key}";
}
Simple, but correctness depends entirely on every call site going through the builder. One direct _db.StringGetAsync("products:all") and the isolation is gone.
2. Separate logical database per tenant
csharp
var db = _multiplexer.GetDatabase(tenantDbIndex);
Redis supports 16 logical databases by default. Isolation is enforced by the connection rather than by convention, but it caps out and the Redis docs discourage the feature.
3. Separate Redis instance per tenant
Strongest isolation, but the operational cost seems disproportionate for tenants that are mostly small.
Questions
For a shared-schema application, is key prefixing considered acceptable in production, or is convention-based isolation regarded as too fragile for tenant data?
Is there an established pattern for enforcing the prefix at the connection or wrapper level, so a developer physically cannot issue an unscoped key?
How does eviction interact with prefixing? With
allkeys-lruon a shared instance, one heavy tenant appears able to evict another's entries. Is per-tenant memory limiting possible without separate instances?
What I've ruled out
IDistributedCachealone, as it gives no hook to enforce scopingSCAN-based invalidation by prefix, which the docs warn against on large keyspacesSeparate instance per tenant, on cost grounds, unless the alternatives are genuinely unsafe
I'm looking for the reasoning behind the choice rather than a preference — specifically what fails at scale with prefixing.