# Data Standard: Redis Cache (Azure Cache for Redis)

> **Scope:** data-nosql-cache
> **Layer:** 2
> **Keywords:** redis, cache, distributed-cache, session, rate-limiting, stackexchange-redis, azure-cache
> **Load When:** caching, session storage, or rate limiting is in scope

**Verified against:** .NET 10 + StackExchange.Redis 2.x + Microsoft.Extensions.Caching.StackExchangeRedis. Last-verified: 2026-05-20.

---

## Overview
Distributed cache for session data, rate limiting, and frequently-read data.

## Setup
```xml
<PackageReference Include="StackExchange.Redis" Version="2.*" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.*" />
```

```csharp
// Program.cs
builder.Services.AddStackExchangeRedisCache(opts =>
{
    opts.Configuration = config["Redis:ConnectionString"]; // from Key Vault
    opts.InstanceName = "myapp:"; // Prefix to avoid key collisions
});
```

## Cache-Aside Pattern
```csharp
public class CachedProductService : IProductService
{
    private readonly IDistributedCache _cache;
    private readonly ApplicationDbContext _db;

    public async Task<Product?> GetAsync(Guid id, CancellationToken ct = default)
    {
        var cacheKey = $"product:{id}";

        // Try cache first
        var cached = await _cache.GetStringAsync(cacheKey, ct);
        if (cached != null)
            return JsonSerializer.Deserialize<Product>(cached);

        // Cache miss → read from DB
        var product = await _db.Products.FindAsync([id], ct);
        if (product == null) return null;

        // Cache with TTL
        await _cache.SetStringAsync(cacheKey,
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
                SlidingExpiration = TimeSpan.FromMinutes(5)
            }, ct);

        return product;
    }

    public async Task InvalidateAsync(Guid id, CancellationToken ct = default)
        => await _cache.RemoveAsync($"product:{id}", ct);
}
```

## TTL Strategy
| Data Type | TTL | Rationale |
|-----------|-----|-----------|
| User session | 30 min sliding | Active session window |
| Product catalog | 15 min absolute | Inventory changes |
| Rate limit counters | 1 min | Per-minute limiting |
| Computed aggregates | 1 hour | Expensive query results |
| Reference data | 24 hours | Config rarely changes |

## Key Naming Convention
```
{service}:{entity}:{id}
{service}:{entity}:list:{filter}
{service}:ratelimit:{userId}:{endpoint}

Examples:
  product:detail:550e8400-e29b-41d4-a716
  product:list:category:electronics
  auth:ratelimit:user123:login
```

## Rate Limiting Pattern
```csharp
public async Task<bool> IsRateLimitedAsync(string userId, string action, int maxPerMinute)
{
    var key = $"ratelimit:{action}:{userId}:{DateTime.UtcNow:yyyyMMddHHmm}";
    var db = _redis.GetDatabase();

    var count = await db.StringIncrementAsync(key);
    if (count == 1) await db.KeyExpireAsync(key, TimeSpan.FromMinutes(1));

    return count > maxPerMinute;
}
```

## Anti-Patterns
- Never cache sensitive data (passwords, tokens, PII) without encryption
- Never use Redis as primary store — it's a cache, not a database
- Never omit TTL — unbounded keys fill memory
- Never catch RedisException silently — degrade gracefully to DB
