In high-concurrency systems, naive caching strategies quickly lead to catastrophic outages:
- Redis Network Bottlenecks: Every read makes an external network hop to Redis, saturating network interfaces;
- Cache Stampedes: When hot keys expire, thousands of concurrent requests hammer the database at once, causing CPU spikes;
- Cross-Tenant Data Leakage: Inconsistent key naming risks leaking Tenant A’s private data to Tenant B!
BitzOrcas.Modern solves this with FusionCache L1 (in-memory) + L2 (Redis) caching: Providing sub-millisecond local reads, single-flight stampede protection, and automated tenant key generation via ICacheKeyBuilder.
Multi-Level Caching Architecture
Step 1: Building Tenant-Safe Keys with ICacheKeyBuilder
Manual string concatenation for cache keys is strictly forbidden. All keys are built via ICacheKeyBuilder, formatting {app}:{env}:v{version}:{area}:{scope}:{parts}:
using BitzOrcas.Application.Abstractions.Caching;
public sealed class CatalogOfferingCacheService( ICacheKeyBuilder cacheKeys, ICacheStore cache){ public async Task<OfferingView?> GetOfferingAsync(string offeringId, CancellationToken ct) { // ① Explicitly assign tenant scope; tenant ID is injected from ICurrentUserAccessor var key = cacheKeys.Build( area: "catalog-offering", scope: CacheScope.Tenant, version: 1, offeringId);
// ② Configure policy: 10m TTL, 20s negative TTL, 10% jitter to prevent thundering herd var policy = new CachePolicy(TimeSpan.FromMinutes(10)) { NegativeTtl = TimeSpan.FromSeconds(20), JitterRatio = 0.10, AreaTag = "catalog", Tags = [$"offering:{offeringId}"] };
// ③ Read-through with single-flight execution to eliminate stampedes var result = await cache.GetOrCreateAsync( key, async token => await LoadFromDbAsync(offeringId, token), policy, ct);
return result.Value; }
private Task<OfferingView?> LoadFromDbAsync(string id, CancellationToken ct) => Task.FromResult<OfferingView?>(null);}Step 2: Three-State Reads and Negative Caching
BitzOrcas handles Hit, Negative, and Miss states explicitly to prevent penetration on non-existent records:
var cached = await cache.TryGetAsync<OfferingView>(key, cancellationToken);
if (cached.HasValue){ // 1. Cache hit with valid entity return Result<OfferingView>.Success(cached.Value!);}
if (cached.IsNegative){ // 2. Negative hit: entity is known to not exist, short-circuit 404 return Result<OfferingView>.Failure(OfferingErrors.NotFound);}
// 3. True Miss: execute query against databasereturn await LoadFromDbAndPopulateAsync(offeringId, cancellationToken);Step 3: Precise Post-Commit Tag Invalidation
Never invalidate cache before the database transaction commits. If the transaction rolls back, the cache would be erroneously purged:
public async ValueTask<Result> Handle(UpdateOfferingCommand command, CancellationToken ct){ // 1. Persist aggregate changes (TransactionPipeline auto-commits transaction) var saveResult = await repository.SaveAsync(offering, ct); if (saveResult.IsFailure) return saveResult;
// 2. Broadcast invalidation across instances using tag descriptors await cache.RemoveByTagAsync($"offering:{command.Id}", ct);
return Result.Success();}Summary
FusionCache multi-level caching gives BitzOrcas speed and resilience:
- L1 Microsecond Response: Sub-millisecond reads straight from process memory;
- Single-Flight Lock: Prevents database stampedes during high-load cold starts;
- Strict Tenant Safety: KeyBuilder enforces multi-tenant boundary compliance.