Multi-layer caching with Redis cluster, cache invalidation patterns, CDN for static assets, and session storage to achieve sub-200ms dashboard load times.
Caching reduces database load and improves response times. ReqVise uses a multi-layer caching strategy with Redis for application data, CDN for static assets, and browser caching for API responses.
| Layer | Data Type | TTL | Invalidation |
|---|---|---|---|
| Browser Cache | API responses (GET) | 5-15 min | ETag mismatch |
| CDN | JS, CSS, images, fonts | 24 hours | Cache-busting hash |
| Redis - Hot Data | User sessions, permissions | 8 hours | On logout/update |
| Redis - Master Data | Categories, UOMs, currencies | 1 hour | Admin update |
| Redis - Reports | Dashboard KPIs, aggregations | 15 min | Scheduled refresh |
Application checks cache first, fetches from database on miss, then populates cache.
// Cache-Aside Pattern Implementation public class CachedVendorService : IVendorService { private readonly IDistributedCache _cache; private readonly IVendorRepository _repository; private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(15); public async Task<Vendor> GetByIdAsync(Guid id) { var cacheKey = $"vendor:{id}"; // 1. Check cache var cached = await _cache.GetStringAsync(cacheKey); if (cached != null) { return JsonSerializer.Deserialize<Vendor>(cached); } // 2. Cache miss - fetch from database var vendor = await _repository.GetByIdAsync(id); if (vendor == null) return null; // 3. Populate cache await _cache.SetStringAsync( cacheKey, JsonSerializer.Serialize(vendor), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = _cacheDuration } ); return vendor; } public async Task UpdateAsync(Vendor vendor) { await _repository.UpdateAsync(vendor); // Invalidate cache on update await _cache.RemoveAsync($"vendor:{vendor.Id}"); await _cache.RemoveAsync($"vendors:tenant:{vendor.TenantId}"); } }
| Entity | Key Pattern | Example |
|---|---|---|
| Single Entity | {entity}:{id} | vendor:abc123 |
| List by Tenant | {entity}s:tenant:{tenantId} | vendors:tenant:xyz789 |
| User Session | session:{userId} | session:user123 |
| User Permissions | perms:{userId} | perms:user123 |
| Dashboard KPIs | kpi:{tenantId}:{period} | kpi:xyz789:2026-Q1 |
| Master Data | master:{type} | master:categories |
| Event | Keys to Invalidate | Strategy |
|---|---|---|
| Vendor Updated | vendor:{id}, vendors:tenant:* | Immediate |
| PO Approved | kpi:{tenantId}:* | Immediate |
| User Logout | session:{userId}, perms:{userId} | Immediate |
| Role Changed | perms:* (tenant-scoped) | Immediate |
| Category Added | master:categories | Immediate |
| Dashboard Refresh | All KPI keys | Scheduled (15 min) |
// Event-driven cache invalidation public class CacheInvalidationHandler : INotificationHandler<VendorUpdatedEvent>, INotificationHandler<POApprovedEvent> { private readonly IDistributedCache _cache; public async Task Handle(VendorUpdatedEvent notification, CancellationToken ct) { // Invalidate specific vendor cache await _cache.RemoveAsync($"vendor:{notification.VendorId}"); // Invalidate tenant vendor list await _cache.RemoveAsync($"vendors:tenant:{notification.TenantId}"); } public async Task Handle(POApprovedEvent notification, CancellationToken ct) { // Invalidate all KPI caches for this tenant var keys = await GetKeysByPatternAsync($"kpi:{notification.TenantId}:*"); foreach (var key in keys) { await _cache.RemoveAsync(key); } } }
Cache invalidation MUST be event-driven. Stale data tolerance: 15 minutes for KPIs, 0 for user permissions.
| Asset Type | Cache-Control | CDN TTL | Versioning |
|---|---|---|---|
| JS Bundles | public, max-age=31536000, immutable | 1 year | Content hash in filename |
| CSS Files | public, max-age=31536000, immutable | 1 year | Content hash in filename |
| Images (static) | public, max-age=86400 | 24 hours | None |
| Fonts | public, max-age=31536000, immutable | 1 year | Version in path |
| API Responses | private, max-age=300 | N/A | ETag |
// Angular build configuration (angular.json) { "outputHashing": "all", // Adds content hash to filenames "optimization": true, "buildOptimizer": true } // Generated files: // main.abc123def.js // styles.xyz789ghi.css // ASP.NET Core CDN headers app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = ctx => { if (ctx.File.Name.EndsWith(".js") || ctx.File.Name.EndsWith(".css")) { ctx.Context.Response.Headers.Append( "Cache-Control", "public, max-age=31536000, immutable" ); } } });
Use ETags for conditional GET requests to reduce bandwidth when data hasn't changed.
// ETag middleware for API responses public class ETagMiddleware { public async Task InvokeAsync(HttpContext context, RequestDelegate next) { if (context.Request.Method != HttpMethods.Get) { await next(context); return; } // Capture response var originalStream = context.Response.Body; using var memStream = new MemoryStream(); context.Response.Body = memStream; await next(context); // Calculate ETag from response body memStream.Seek(0, SeekOrigin.Begin); var hash = MD5.HashData(memStream.ToArray()); var etag = $"\"{Convert.ToHexString(hash)}\""; // Check If-None-Match header if (context.Request.Headers.IfNoneMatch == etag) { context.Response.StatusCode = 304; // Not Modified return; } context.Response.Headers.ETag = etag; memStream.Seek(0, SeekOrigin.Begin); await memStream.CopyToAsync(originalStream); } }
| Rule ID | Category | Description |
|---|---|---|
| BR-CACHE-001 | Invalidation | Cache invalidation MUST be event-driven, not time-based only |
| BR-CACHE-002 | Permissions | User permissions cache MUST be invalidated immediately on role change |
| BR-CACHE-003 | KPIs | Dashboard KPIs MAY be stale up to 15 minutes |
| BR-CACHE-004 | Static Assets | JS/CSS MUST use content hash for cache busting |
| BR-CACHE-005 | Redis | Redis cluster MUST have 3 nodes for high availability |
| BR-CACHE-006 | Fallback | Cache miss MUST NOT cause request failure - fallback to database |