.claude/skills/caching-strategy/SKILL.md
Use when deciding between IMemoryCache, IDistributedCache, Response Caching, or Output Caching, or when caching is applied inconsistently across a codebase and a decision framework is needed. Covers a decision matrix by data volatility and deployment topology, cache key design conventions, invalidation strategies (TTL, event-based, versioned, tag-based), and stampede prevention with GetOrCreateAsync. Domain: Performance, Architecture. Level: Intermediate. Tags: caching, IMemoryCache, IDistributedCache, Redis, performance, output-caching.
npx skillsauth add klod68/littlerae caching-strategyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Teams choose caching strategies ad-hoc, leading to inconsistencies: some code uses ConcurrentDictionary, others use IMemoryCache, some use IDistributedCache, and cache invalidation is an afterthought. Without a clear decision framework, caches either leak memory, serve stale data, or don't exist where they should.
Select the caching layer based on data volatility, consistency requirements, and deployment topology.
| Factor | IMemoryCache | IDistributedCache | Response Caching | Output Caching (.NET 7+) | |--------|-------------|-------------------|------------------|-------------------------| | Scope | Single process | Cross-process / cross-server | HTTP client/proxy | Server-side HTTP | | Speed | Fastest (in-process) | Network round-trip | No server hit | Cache at endpoint level | | Consistency | Per-instance (diverges in scale-out) | Shared across instances | Stale until expiry | Stale until expiry | | Best for | Reference data, config, computations | Session state, shared lookups | Static content, CDN | API responses, pages | | Invalidation | Explicit or time-based | Explicit or time-based | Cache-Control headers | Tag-based or time-based |
| Scenario | Cache Type | Why |
|----------|-----------|-----|
| Lookup table rarely changes (countries, currencies) | IMemoryCache | In-process, no network hop, stale data acceptable |
| User session across load-balanced servers | IDistributedCache (Redis) | Must be shared across instances |
| Expensive computation result (per request) | IMemoryCache | Short TTL, per-instance is fine |
| API response that changes daily | Output Caching | Server-side, tag-based invalidation |
| Static assets (images, CSS, JS) | Response Caching | Let browsers and CDNs cache |
| Feature flag state | IMemoryCache with short TTL | Per-instance, eventual consistency acceptable |
| Search results with pagination | IDistributedCache | Shared across instances, keyed by query + page |
public class CountryService
{
private readonly IMemoryCache _cache;
private static readonly TimeSpan CacheDuration = TimeSpan.FromHours(1);
public async Task<IReadOnlyList<Country>> GetCountriesAsync(CancellationToken ct)
{
return await _cache.GetOrCreateAsync("countries:all", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
entry.Size = 1; // Required if SizeLimit is configured
return await _repository.GetAllCountriesAsync(ct);
}) ?? [];
}
}
public async Task<UserProfile?> GetProfileAsync(int userId, CancellationToken ct)
{
var key = $"profile:{userId}";
var cached = await _distributedCache.GetStringAsync(key, ct);
if (cached is not null)
return JsonSerializer.Deserialize<UserProfile>(cached);
var profile = await _repository.GetProfileAsync(userId, ct);
if (profile is not null)
{
await _distributedCache.SetStringAsync(key,
JsonSerializer.Serialize(profile),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15),
SlidingExpiration = TimeSpan.FromMinutes(5)
}, ct);
}
return profile;
}
{entity}:{discriminator}:{id} — e.g., orders:status:active, user:42:profile| Strategy | When to Use | Example |
|----------|------------|---------|
| Time-based (TTL) | Data that changes predictably | Refresh config every 5 minutes |
| Event-based | Data changed by known operations | Invalidate on OrderUpdated event |
| Versioned keys | Data with infrequent bulk changes | countries:v3 — bump version on update |
| Tag-based | Groups of related cached items | Invalidate all orders:* on bulk update |
Dictionary with no TTL needsIMemoryCache (SizeLimit option) — unbounded caches leak memory.AbsoluteExpiration (max lifetime) and SlidingExpiration (idle timeout).SemaphoreSlim or GetOrCreateAsync — not manual check-then-set.Debug level for diagnostics.| Pattern | Problem | Correction |
|---------|---------|-----------|
| ConcurrentDictionary as unbounded cache | Memory leak — entries never expire | Use IMemoryCache with expiration |
| Check-then-set without lock | Cache stampede under concurrency | Use GetOrCreateAsync or SemaphoreSlim |
| Caching mutable objects | Shared reference corruption | Cache serialized copy or use immutable types |
| Same TTL for all cached data | Over-caching volatile data | Match TTL to data volatility |
| Distributed cache for in-process-only data | Unnecessary network round-trip | Use IMemoryCache for process-local data |
| No expiration on cached items | Stale data served indefinitely | Always set AbsoluteExpirationRelativeToNow |
tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.