Skip to content

Case 8: Stop Re-calculating - Caching Hot Data with IMemoryCache

The Scenario 📝

  • System: An e-commerce website. The main navigation menu of the site displays a list of all product Categories.
  • Problem: This list is shown on almost every page of the website. The data is nearly static, changing only a few times a day when an administrator adds or edits a category. However, the application is querying the database for this list on every single request.

The Problematic Code (Fetching Every Time)

csharp
public class NavigationService
{
    private readonly AppDbContext _context;

    public NavigationService(AppDbContext context)
    {
        _context = context;
    }

    public async Task<List<Category>> GetCategoriesAsync()
    {
        // This query runs thousands of times per minute under high load
        return await _context.Categories
                             .AsNoTracking()
                             .OrderBy(c => c.Name)
                             .ToListAsync();
    }
}

The Hidden Bottleneck 🧐

  1. Unnecessary Database Load: Even though each individual query for categories is fast, running it thousands of times per minute creates a significant load on the database.
  2. Wasted Resources: Every query uses a database connection, consumes CPU, and adds unnecessary network latency to every request.
  3. The Core Problem: The application is constantly asking a question it already knows the answer to. The category data almost never changes.

The Solution: Use In-Memory Caching with IMemoryCache

  • The Logic: We will use the "Cache-Aside" pattern.

    1. Check if the data is in the cache.
    2. If yes, return the data from the cache immediately.
    3. If no, get the data from the database, save it to the cache for next time, and then return it.
  • IMemoryCache is a built-in service in ASP.NET Core that is very easy to use.

  • The Optimized Code:

    csharp
    public class NavigationService
    {
        private readonly AppDbContext _context;
        private readonly IMemoryCache _cache; // Inject IMemoryCache
        private const string CategoriesCacheKey = "NavigationCategories";
    
        public NavigationService(AppDbContext context, IMemoryCache cache)
        {
            _context = context;
            _cache = cache;
        }
    
        public async Task<List<Category>> GetCategoriesAsync()
        {
            // Use GetOrCreateAsync to implement the cache-aside pattern safely and cleanly
            return await _cache.GetOrCreateAsync(CategoriesCacheKey, async entry =>
            {
                // Set a cache expiration time, for example, 1 hour
                entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
    
                // This logic only runs when the data is not in the cache (a cache miss)
                return await _context.Categories
                                     .AsNoTracking()
                                     .OrderBy(c => c.Name)
                                     .ToListAsync();
            });
        }
    }

The Results ✨

  1. Drastically Reduced Database Load: After the first request, for the next hour, 100% of all other requests will get the data from the application's memory without touching the database.
  2. Faster Response Times: Reading from RAM is many times faster than making a round-trip to the database. The website will load faster.
  3. Increased Scalability: The database is freed from repetitive queries, allowing it to use its resources for more important transactions.

Conclusion:

  • Golden Rule: "Don't fetch what you already have."
  • Caching is one of the most effective strategies for improving web application performance.
  • IMemoryCache is a simple, built-in tool for caching "hot data"—data that is accessed frequently but changes rarely.