In-Memory Caching
A lightweight, thread-safe cache for data that's expensive to compute but fine to serve slightly stale โ leaderboard rankings, aggregate stats, anything read far more often than it changes. No Redis, no distributed cache, just a locked dictionary with a TTL.
The shape
Key pieces: a private cache entry type carrying a timestamp, a lock object guarding all access, and a TTL check on read. Writes that should invalidate the cache call InvalidateCache explicitly rather than relying on expiry alone.
LeaderboardService.cs
public class LeaderboardService
{
private readonly Dictionary<Guid, LeaderboardCache> _cache = [];
private readonly object _lock = new();
private class LeaderboardCache
{
public DateTime CachedAt { get; set; }
public List<LeaderboardEntry> Entries { get; set; } = [];
}
public List<LeaderboardEntry> GetCategoryLeaderboard(
Guid categoryId, IEnumerable<Rating> ratings, int cacheDurationMinutes = 5)
{
lock (_lock)
{
if (_cache.TryGetValue(categoryId, out var cached) &&
DateTime.UtcNow - cached.CachedAt < TimeSpan.FromMinutes(cacheDurationMinutes))
{
return cached.Entries;
}
var entries = BuildEntries(ratings);
_cache[categoryId] = new LeaderboardCache { CachedAt = DateTime.UtcNow, Entries = entries };
return entries;
}
}
public void InvalidateCache(Guid categoryId)
{
lock (_lock)
{
_cache.Remove(categoryId);
}
}
private static List<LeaderboardEntry> BuildEntries(IEnumerable<Rating> ratings)
=> ratings
.OrderByDescending(r => r.CurrentRating)
.Select((r, i) => new LeaderboardEntry { UserId = r.UserId, Rank = i + 1 })
.ToList();
}
Invalidate on write
Whatever mutates the underlying data is responsible for clearing the relevant cache entry. This keeps the caching concern out of the read path's business logic โ the write-side service just calls one method after it's done.
usage
public async Task RecordMatchAsync(RecordMatchRequest request)
{
// ... update ratings, save match ...
_leaderboardService.InvalidateCache(request.CategoryId);
}
When this is the right tool
- Single-instance deployment (a locked in-process dictionary doesn't sync across servers โ for multi-instance, reach for a distributed cache instead).
- Read-heavy, write-light data where a few minutes of staleness is acceptable.
- You want zero extra infrastructure (no Redis connection string, no extra moving part to monitor).