14

We practice CQRS architecture in our application, i.e. we have a number of classes implementing ICommand and there are handlers for each command: ICommandHandler<ICommand>. Same way goes for data retrieval - we have IQUery<TResult> with IQueryHandler<IQuery, TResult>. Pretty common these days.

Some queries are used very often (for multiple drop downs on pages) and it makes sense to cache the result of their execution. So we have a decorator around IQueryHandler that caches some query executions.
Queries implement interface ICachedQuery and decorator caches the results. Like this:

public interface ICachedQuery {
    String CacheKey { get; }
    int CacheDurationMinutes { get; }
}

public class CachedQueryHandlerDecorator<TQuery, TResult> 
    : IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    private IQueryHandler<TQuery, TResult> decorated;
    private readonly ICacheProvider cacheProvider;

    public CachedQueryHandlerDecorator(IQueryHandler<TQuery, TResult> decorated, 
        ICacheProvider cacheProvider) {
        this.decorated = decorated;
        this.cacheProvider = cacheProvider;
    }

    public TResult Handle(TQuery query) {
        var cachedQuery = query as ICachedQuery;
        if (cachedQuery == null)
            return decorated.Handle(query);

        var cachedResult = (TResult)cacheProvider.Get(cachedQuery.CacheKey);

        if (cachedResult == null)
        {
            cachedResult = decorated.Handle(query);
            cacheProvider.Set(cachedQuery.CacheKey, cachedResult, 
                cachedQuery.CacheDurationMinutes);
        }

        return cachedResult;
    }
}

There was a debate whether we should have an interface on queries or an attribute. Interface is currently used because you can programmatically change the cache key depending on what is being cached. I.e. you can add entities' id into cache key (i.e. have keys like "person_55", "person_56", etc.).

The issue is of course with cache invalidation (naming and cache invalidation, eh?). Problem with that is that queries do not match one-to-one with commands or entities. And execution of a single command (i.e modification of a person record) should render invalid multiple cache records: person record and drop down with persons' names.

At the moment I have a several candidates for the solution:

  1. Have all the cache keys recorded somehow in entity class, mark the entity as ICacheRelated and return all these keys as part of this interface. And when EntityFramework is updating/creating the record, get these cache keys and invalidate them. (Hacky!)
  2. Commands should be invalidating all the caches. Or rather have ICacheInvalidatingCommand that should return list of cache keys that should be invalidated. And have a decorator on ICommandHandler that will invalidate the cache when the command is executed.
  3. Don't invalidate the caches, just set short cache lifetimes (how short?)
  4. Magic beans.

I don't like any of the options (maybe apart from number 4). But I think that option 2 is one I'll give a go. Problem with this, cache key generation becomes messy, I'll need to have a common place between commands and queries that know how to generate keys. Another issue would that it'll be too easy to add another cached query and miss the invalidating part on commands (or not all commands that should invalidate will invalidate).

Any better suggestions?

Steven
  • 166,672
  • 24
  • 332
  • 435
trailmax
  • 34,305
  • 22
  • 140
  • 234

2 Answers2

13

I'm wondering whether you should really do caching here at all, since SQL server is pretty good in caching results, so you should see queries that return a fixed list of drop down values to be really fast.

Of course, when you do caching, it depends on the data what the cache duration should be. It depends on how the system is used. For instance, if new values are added by an administrator, it's easy to explain that it takes a few minutes before other users will see his changes.

If, on the other hand, a normal user is expected to add values, while working with a screen that has such list, things might be different. But in that case, it might even be good to make the experience for the user more fluent, by presenting him with the drop down or giving him the option to add a new value right there. That new value is than processed in the same transaction and everything will be fine.

If you want to do cache invalidation however, I would say you need to let your commands publish domain events. This way other independent parts of the system can react to this operation and can do (among other things) the cache invalidation.

For instance:

public class AddCityCommandHandler : ICommandHandler<AddCityCommand>
{
    private readonly IRepository<City> cityRepository;
    private readonly IGuidProvider guidProvider;
    private readonly IDomainEventPublisher eventPublisher;

    public AddCountryCommandHandler(IRepository<City> cityRepository,
        IGuidProvider guidProvider, IDomainEventPublisher eventPublisher) { ... }

    public void Handle(AddCityCommand command)
    {
        City city = cityRepository.Create();

        city.Id = this.guidProvider.NewGuid();
        city.CountryId = command.CountryId;

        this.eventPublisher.Publish(new CityAdded(city.Id));
    }
}

Here you publish the CityAdded event which might look like this:

public class CityAdded : IDomainEvent
{
    public readonly Guid CityId;

    public CityAdded (Guid cityId) {
        if (cityId == Guid.Empty) throw new ArgumentException();
        this.CityId = cityId;
    }
}

Now you can have zero or more subscribers for this event:

public class InvalidateGetCitiesByCountryQueryCache : IEventHandler<CityAdded>
{
    private readonly IQueryCache queryCache;
    private readonly IRepository<City> cityRepository;

    public InvalidateGetCitiesByCountryQueryCache(...) { ... }

    public void Handle(CityAdded e)
    {
        Guid countryId = this.cityRepository.GetById(e.CityId).CountryId;

        this.queryCache.Invalidate(new GetCitiesByCountryQuery(countryId));
    }
}

Here we have special event handler that handles the CityAdded domain event just to invalide the cache for the GetCitiesByCountryQuery. The IQueryCache here is an abstraction specially crafted for caching and invalidating query results. The InvalidateGetCitiesByCountryQueryCache explicitly creates the query who's results should be invalided. This Invalidate method can than make use of the ICachedQuery interface to determine its key and invalide the results (if any).

Instead of using the ICachedQuery to determine the key however, I just serialize the whole query to JSON and use that as key. This way each query with unique parameters will automatically get its own key and cache, and you don't have to implement this on the query itself. This is a very safe mechanism. However, in case your cache should survive AppDomain recycles, you need to make sure that you get exactly the same key across app restarts (which means the ordering of the serialized properties must be guaranteed).

One thing you must keep in mind though is that this mechanism is especially suited in case of eventual consistency. To take the previous example, when do you want to invalidate the cache? Before you added the city or after? If you invalidate the cache just before, it's possible that the cache is repopulated before you do the commit. That would suck of course. On the other hand, if you do it just after, it's possible that someone still observes the old value directly after. Especially when your events are queued and processed in the background.

But what you can do is execute the queued events directly after you did the commit. You can use a command handler decorator for that:

public class EventProcessorCommandHandlerDecorator<T> : ICommandHandler<T>
{
    private readonly EventPublisherImpl eventPublisher;
    private readonly IEventProcessor eventProcessor;
    private readonly ICommandHandler<T> decoratee;

    public void Handle(T command)
    {
        this.decotatee.Handle(command);

        foreach (IDomainEvent e in this.eventPublisher.GetQueuedEvents())
        {
            this.eventProcessor.Process(e);
        }
    }
}

Here the decorator depends directly on the event publisher implementation to allow calling the GetQueuedEvents() method that would be unavailable from the IDomainEventPublisher interface. And we iterate all events and pass those events on to the IEventProcessor mediator (which just works as the IQueryProcessor does).

Do note a few things about this implementation though. It's NOT transactional. If you need to be sure that all your events get processed, you need to store them in a transactional queue and process them from there. For cache invalidation however, it doesn't seem like a big problem to me.

This design might seem like overkill just for caching, but once you started publishing domain events, you'll start to see many use cases for them that will make working with your system considerably simpler.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • 1
    I like your trick to have Json-serialised string as a cache key - very neat! As for events, we already have static call to `DomainEvents.Raise()` and domain events handler, very similar to what you describe here, so no overkill. Just need to have a generic `ClearCacheEvent(IQuery query)` that will do the cache invalidation. – trailmax Oct 15 '14 at 13:14
  • As for the need of cache vs SQL server caching: we are in Azure and SQL Server usually sits on the other virtual machine, probably in other end of the data-center. And network latency starts to kick-in when we have 10+ drop-downs, even if drop-downs are simple `select * from ProductTypes` with 30 records in the table. – trailmax Oct 15 '14 at 13:17
  • 1
    Next time I'll be doing caching, I'll implement your stuff and will let you know how it works out in production. Thanks for your answer! – trailmax Oct 15 '14 at 13:20
  • @trailmax: I surely hope that this `DomainEvents` is not some static thing. I know it comes from an old Udi Dahan blog, but even he has discarded this design. – Steven Oct 15 '14 at 13:28
  • That's where it comes from indeed. Right, we'll need to update our approach and brush-up on events. It was causing a bit of trouble indeed. – trailmax Oct 15 '14 at 13:33
  • 2
    While you could go the event route, I would probably just create another CommandHandlerCacheInvalidationDecorator that took a list of ICommandHandlerInvalidator or something that each command would create when needed (similar to the IValidator/Decorator solution). Then each command would deal with clearing the cache of items it knows about after running the command. Though, you have to be careful if you Json the class contents as the key itself. You'd want to add on the full namespace of the Query, too, so you could remove all the queries that are affected. – Daniel Lorenz Aug 01 '17 at 16:21
1

Are you using a separate read and write model? If so, perhaps your "projection" classes (the ones that handle events from the write model and do CRUD on the read model) could invalidate the appropriate cache entries at the same time.

pnschofield
  • 877
  • 1
  • 6
  • 16