Isaac.

Implementing Caching Strategies

Add effective caching to improve application performance.

By EMEPublished: February 20, 2025
cachingredisdistributed cachememoization

A Simple Analogy

Caching is like keeping frequently-used items within reach. Don't go to the warehouse every time; grab from nearby.


Why Cache?

  • Performance: Avoid expensive operations
  • Scalability: Reduce database load
  • UX: Faster response times
  • Cost: Cheaper compute resources
  • Reliability: Handle traffic spikes

In-Memory Cache

public class ProductService
{
    private readonly IMemoryCache _cache;
    private readonly IProductRepository _repository;
    
    public async Task<Product> GetProductAsync(int id)
    {
        var cacheKey = $"product-{id}";
        
        if (_cache.TryGetValue(cacheKey, out var cached))
            return cached as Product;
        
        var product = await _repository.GetAsync(id);
        
        var options = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromHours(1))
            .SetSlidingExpiration(TimeSpan.FromMinutes(20));
        
        _cache.Set(cacheKey, product, options);
        
        return product;
    }
}

Distributed Cache (Redis)

public class ProductService
{
    private readonly IDistributedCache _cache;
    private readonly IProductRepository _repository;
    
    public async Task<Product> GetProductAsync(int id)
    {
        var cacheKey = $"product-{id}";
        var cached = await _cache.GetStringAsync(cacheKey);
        
        if (cached != null)
            return JsonSerializer.Deserialize<Product>(cached);
        
        var product = await _repository.GetAsync(id);
        
        await _cache.SetStringAsync(
            cacheKey,
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
            }
        );
        
        return product;
    }
}

Cache Invalidation

public async Task UpdateProductAsync(int id, UpdateProductRequest request)
{
    var product = new Product { Id = id, Name = request.Name };
    await _repository.UpdateAsync(product);
    
    // Invalidate cache
    await _cache.RemoveAsync($"product-{id}");
    await _cache.RemoveAsync("products-list");
}

HTTP Caching Headers

[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
    var product = await _service.GetProductAsync(id);
    
    // Cache in browser for 1 hour
    Response.Headers.CacheControl = "public, max-age=3600";
    Response.Headers.ETag = $"\"{product.UpdatedAt.GetHashCode()}\"";
    
    return Ok(product);
}

Best Practices

  1. TTL: Set reasonable expiration times
  2. Invalidation: Remove when data changes
  3. Keys: Use consistent naming
  4. Size: Don't cache too much
  5. Monitoring: Track hit rates

Related Concepts

  • Cache-aside pattern
  • Write-through cache
  • Cache warming
  • Cache stampede

Summary

Implement multi-level caching with in-memory and distributed caches. Use appropriate TTLs and invalidation strategies.