Skip to Content
Course content

218: Practice Exercise: Building a Simple Caching Decorator

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Caching is one of those things that looks trivial on a whiteboard but becomes a nightmare in production if you aren't careful. When we use the Decorator pattern for caching, we're trying to add a layer of performance without polluting the actual business logic of our service. But it's easy to write a decorator that "looks" right but does absolutely nothing.

The "Ghost Cache" Problem

I recently saw a junior dev commit a caching decorator that passed all the unit tests but didn't actually improve performance in the staging environment. The code looked like this:

public class CachingProductService : IProductService
{
    private readonly IProductService _innerService;
    private readonly Dictionary<int, Product> _cache = new();

    public CachingProductService(IProductService innerService)
    {
        _innerService = innerService;
    }

    public Product GetProduct(int id)
    {
        if (_cache.ContainsKey(id))
        {
            return _cache[id];
        }

        // Fetch from the "slow" database service
        var product = _innerService.GetProduct(id);
        return product;
    }
}

At first glance, it's clean. It checks the dictionary, and if the item isn't there, it calls the inner service. But there's a glaring hole here: we're fetching the product from the inner service, but we're never actually putting it back into the cache. Every single call to GetProduct results in a cache miss, meaning we're paying the overhead of the dictionary lookup only to perform the expensive database call anyway. It's a "ghost cache"—it exists in the code, but it never actually stores anything.

Closing the Loop with Assignment

To fix this, we need to ensure that once we've done the hard work of fetching the data, we store it for the next caller. I also want to point out that using a standard Dictionary in a service like this is a ticking time bomb if your app is multi-threaded. In a real C# app, you'll almost always want a ConcurrentDictionary to avoid those nasty InvalidOperationException crashes when two threads try to write to the cache at once.

using System.Collections.Concurrent;

public class CachingProductService : IProductService
{
    private readonly IProductService _innerService;
    // Use ConcurrentDictionary for thread-safety
    private readonly ConcurrentDictionary<int, Product> _cache = new();

    public CachingProductService(IProductService innerService)
    {
        _innerService = innerService;
    }

    public Product GetProduct(int id)
    {
        // GetOrAdd is a shorthand that checks the key and 
        // executes the factory function if the key is missing
        return _cache.GetOrAdd(id, key => _innerService.GetProduct(key));
    }
}

By using GetOrAdd, we've collapsed the "check, fetch, and save" logic into a single, atomic operation. It's cleaner, thread-safe, and—most importantly—it actually caches the data. Now, the first call is slow, and every subsequent call for that ID is nearly instantaneous.




📋 Practical Task

Exercise: Implementing a Time-Expiring User Profile Cache

In a real system, you can't cache data forever because it eventually becomes stale. Your task is to build a caching decorator for a user profile system that implements a basic "expiration" mechanism.

Requirements:

  • Create an IUserProfileService interface with a method UserProfile GetProfile(int userId).
  • Create a UserProfileService (the "slow" version) that simulates a delay using Thread.Sleep(1000).
  • Create a CachingUserProfileDecorator that wraps the service.
  • Instead of a simple ConcurrentDictionary<int, UserProfile>, use a dictionary that stores a tuple or a small helper class containing both the UserProfile and a DateTime timestamp of when it was cached.
  • The decorator should only return the cached value if it is less than 10 seconds old. If the data is older than 10 seconds (or doesn't exist), it must fetch fresh data from the inner service and update the cache.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.