Back to ER Diagram
Caching Strategy

Caching Strategy

Multi-layer caching with Redis cluster, cache invalidation patterns, CDN for static assets, and session storage to achieve sub-200ms dashboard load times.

Redis 8.x
Azure CDN
ETag
TTL Management

Overview

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.

<200ms
Dashboard Target
90%
Cache Hit Rate
15 min
Default TTL
24 hrs
CDN TTL

Cache Layers

Browser
ETag/Cache-Control
CDN
Static Assets
Redis
Application Cache
PostgreSQL
Database
LayerData TypeTTLInvalidation
Browser CacheAPI responses (GET)5-15 minETag mismatch
CDNJS, CSS, images, fonts24 hoursCache-busting hash
Redis - Hot DataUser sessions, permissions8 hoursOn logout/update
Redis - Master DataCategories, UOMs, currencies1 hourAdmin update
Redis - ReportsDashboard KPIs, aggregations15 minScheduled refresh

Redis Caching Patterns

Cache-Aside Pattern

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}");
    }
}

Cache Keys Convention

EntityKey PatternExample
Single Entity{entity}:{id}vendor:abc123
List by Tenant{entity}s:tenant:{tenantId}vendors:tenant:xyz789
User Sessionsession:{userId}session:user123
User Permissionsperms:{userId}perms:user123
Dashboard KPIskpi:{tenantId}:{period}kpi:xyz789:2026-Q1
Master Datamaster:{type}master:categories

Cache Invalidation

EventKeys to InvalidateStrategy
Vendor Updatedvendor:{id}, vendors:tenant:*Immediate
PO Approvedkpi:{tenantId}:*Immediate
User Logoutsession:{userId}, perms:{userId}Immediate
Role Changedperms:* (tenant-scoped)Immediate
Category Addedmaster:categoriesImmediate
Dashboard RefreshAll KPI keysScheduled (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);
        }
    }
}

Business Rule: BR-CACHE-001

Cache invalidation MUST be event-driven. Stale data tolerance: 15 minutes for KPIs, 0 for user permissions.

CDN Configuration

Asset TypeCache-ControlCDN TTLVersioning
JS Bundlespublic, max-age=31536000, immutable1 yearContent hash in filename
CSS Filespublic, max-age=31536000, immutable1 yearContent hash in filename
Images (static)public, max-age=8640024 hoursNone
Fontspublic, max-age=31536000, immutable1 yearVersion in path
API Responsesprivate, max-age=300N/AETag
// 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"
            );
        }
    }
});

ETag for API Caching

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);
    }
}

Business Rules Summary

Rule IDCategoryDescription
BR-CACHE-001InvalidationCache invalidation MUST be event-driven, not time-based only
BR-CACHE-002PermissionsUser permissions cache MUST be invalidated immediately on role change
BR-CACHE-003KPIsDashboard KPIs MAY be stale up to 15 minutes
BR-CACHE-004Static AssetsJS/CSS MUST use content hash for cache busting
BR-CACHE-005RedisRedis cluster MUST have 3 nodes for high availability
BR-CACHE-006FallbackCache miss MUST NOT cause request failure - fallback to database