Core API Response Caching

The BookStore API reads book data from the database on every single request. For a list of books that rarely changes, this means hundreds of identical database queries per minute under high traffic. Response caching stores the result of a request and returns the stored result for subsequent identical requests — dramatically reducing database load and improving response speed.

Types of Caching

TypeWhere Data Is StoredBest For
In-Memory CacheServer's RAMSingle server, fast access
Distributed Cache (Redis)External Redis serverMultiple servers, shared cache
HTTP Response CacheClient browser / CDNStatic or rarely changing public data

This topic covers In-Memory Caching — the simplest approach for the BookStore API.

Step 1 – Register In-Memory Cache

// Program.cs
builder.Services.AddMemoryCache();

Step 2 – Use IMemoryCache in BookService

// Services/BookService.cs
using Microsoft.Extensions.Caching.Memory;

public class BookService : IBookService
{
    private readonly IBookRepository _bookRepository;
    private readonly IMemoryCache _cache;
    private readonly ILogger<BookService> _logger;

    // Cache key constants — avoids typos
    private const string AllBooksCacheKey = "AllBooks";
    private const string BookByIdCacheKeyPrefix = "Book_";

    public BookService(
        IBookRepository bookRepository,
        IMemoryCache cache,
        ILogger<BookService> logger)
    {
        _bookRepository = bookRepository;
        _cache = cache;
        _logger = logger;
    }

    public async Task<List<Book>> GetAllAsync()
    {
        // Try to get from cache first
        if (_cache.TryGetValue(AllBooksCacheKey, out List<Book>? cachedBooks) && cachedBooks != null)
        {
            _logger.LogInformation("Books served from cache.");
            return cachedBooks;
        }

        // Cache miss — fetch from database
        _logger.LogInformation("Cache miss. Fetching books from database.");
        var books = await _bookRepository.GetAllAsync();

        // Store in cache for 5 minutes
        var cacheOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(5))
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));

        _cache.Set(AllBooksCacheKey, books, cacheOptions);

        return books;
    }

    public async Task<Book?> GetByIdAsync(int id)
    {
        string cacheKey = $"{BookByIdCacheKeyPrefix}{id}";

        if (_cache.TryGetValue(cacheKey, out Book? cachedBook) && cachedBook != null)
        {
            _logger.LogInformation("Book {Id} served from cache.", id);
            return cachedBook;
        }

        var book = await _bookRepository.GetByIdAsync(id);

        if (book != null)
        {
            _cache.Set(cacheKey, book, TimeSpan.FromMinutes(5));
        }

        return book;
    }

    public async Task<Book> CreateAsync(Book book)
    {
        book.CreatedDate = DateTime.UtcNow;
        await _bookRepository.AddAsync(book);
        await _bookRepository.SaveAsync();

        // Invalidate the all-books cache — it is now stale
        _cache.Remove(AllBooksCacheKey);
        _logger.LogInformation("Cache invalidated after book creation.");

        return book;
    }

    public async Task<bool> UpdateAsync(int id, Book updatedBook)
    {
        var book = await _bookRepository.GetByIdAsync(id);
        if (book == null) return false;

        book.Title = updatedBook.Title;
        book.Author = updatedBook.Author;
        book.Price = updatedBook.Price;
        book.Category = updatedBook.Category;
        book.IsAvailable = updatedBook.IsAvailable;

        await _bookRepository.UpdateAsync(book);
        await _bookRepository.SaveAsync();

        // Invalidate both caches
        _cache.Remove(AllBooksCacheKey);
        _cache.Remove($"{BookByIdCacheKeyPrefix}{id}");

        return true;
    }

    public async Task<bool> DeleteAsync(int id)
    {
        var book = await _bookRepository.GetByIdAsync(id);
        if (book == null) return false;

        await _bookRepository.DeleteAsync(book);
        await _bookRepository.SaveAsync();

        // Invalidate both caches
        _cache.Remove(AllBooksCacheKey);
        _cache.Remove($"{BookByIdCacheKeyPrefix}{id}");

        return true;
    }
}

Cache Options Explained

var cacheOptions = new MemoryCacheEntryOptions()
    .SetAbsoluteExpiration(TimeSpan.FromMinutes(5))    // Always expires after 5 min
    .SetSlidingExpiration(TimeSpan.FromMinutes(2))     // Expires 2 min after last access
    .SetPriority(CacheItemPriority.Normal);            // Low/Normal/High/NeverRemove
OptionMeaningBookStore Use
AbsoluteExpirationCache entry expires at a fixed time, regardless of accessBook list expires after 5 minutes
SlidingExpirationCache entry resets its timer on each accessIndividual book cache resets if frequently accessed
PriorityWhen memory is low, low-priority items are removed firstBook list is Normal priority

Cache Flow Diagram

Request: GET /api/books

Check Cache: "AllBooks" key exists?
       |
       ├── YES (cache hit)
       │       → Return cached book list
       │         ⏱ ~1ms response time
       │
       └── NO (cache miss)
               → Query database (SELECT * FROM Books)
               → Store result in cache with 5-min expiry
               → Return book list
                 ⏱ ~50-200ms response time

Next request within 5 minutes:
       → Cache hit → ~1ms response time ✓

HTTP Response Caching Headers

In addition to server-side caching, HTTP headers can instruct the client's browser or a CDN to cache the response:

// Program.cs
builder.Services.AddResponseCaching();

// In middleware pipeline
app.UseResponseCaching();
// In the controller action
[HttpGet]
[AllowAnonymous]
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, NoStore = false)]
public async Task<IActionResult> GetAll()
{
    var books = await _bookService.GetAllAsync();
    return Ok(_mapper.Map<List<BookResponseDto>>(books));
}

This adds the following header to the HTTP response:

Cache-Control: public, max-age=60

The browser caches the response for 60 seconds. The same request within that time returns the cached response without hitting the server at all.

Cache Invalidation Strategy for BookStore API

OperationCache Action
GET all booksRead from cache → return. If miss, query DB then store
GET book by IdRead from cache → return. If miss, query DB then store
POST (create book)Remove AllBooks cache — list is stale
PUT (update book)Remove AllBooks and Book_{id} cache
DELETE bookRemove AllBooks and Book_{id} cache

Key Points

  • Caching stores the result of expensive operations (database queries) in memory and returns the stored result for subsequent identical requests.
  • IMemoryCache stores data in the server's RAM — fast, but not shared across multiple server instances.
  • Cache must be invalidated (removed) whenever the underlying data changes — after create, update, or delete operations.
  • Absolute expiration removes the cache entry after a fixed duration; sliding expiration resets the timer on each access.
  • HTTP response caching headers (Cache-Control) tell the browser or CDN to cache responses on the client side.

Leave a Comment