Skip to content

Caching Pattern

Server-side HybridCache via MiniatR pipeline with tag-based invalidation.

Overview

flowchart TB
    Q[Query with ICacheableQuery] --> C{HybridCache?}
    C -->|Hit| R1[Return Cached]
    C -->|Miss| H1[Handler]
    H1 --> S[Store with Tags]
    S --> R2[Return Fresh]

    CMD[Command with ICacheInvalidatingCommand] --> H2[Handler]
    H2 --> I[Invalidate by Tags]
    I -.->|Clears tagged entries| C

Server-Side Caching (HybridCache)

Making a Query Cacheable

Implement ICacheableQuery with cache key, duration, and tags:

public sealed class GetCategoriesQuery : IRequest<IList<CategoryResponse>>, IRequestWithCurrentGroupId, ICacheableQuery
{
    public Guid CurrentGroupId { get; set; }

    public string CacheKey => CacheKeys.Categories(CurrentGroupId);
    public TimeSpan CacheDuration => CacheDurations.Categories;
    public IReadOnlyList<string> Tags => [CacheTags.Group(CurrentGroupId), CacheTags.Categories];
}

Cache Invalidation

Commands implement ICacheInvalidatingCommand with tags to invalidate:

public sealed class CreateCategoryCommand : IRequest<CategoryResponse>, IRequestWithCurrentGroupId, ICacheInvalidatingCommand
{
    public CategoryRequest Model { get; }
    public Guid CurrentGroupId { get; set; }

    public IReadOnlyList<string> TagsToInvalidate => [CacheTags.Group(CurrentGroupId), CacheTags.Categories];
}

Constants Structure

Cache configuration is split into separate files in Application/Common/Constants/:

File Purpose
CacheKeys.cs Cache key generators (group-scoped)
CacheTags.cs Tag constants for invalidation
CacheDurations.cs TimeSpan durations
CacheConstants.cs Config (MaxPayloadBytes, MaxKeyLength)
// CacheKeys.cs
public static string Categories(Guid groupId) => $"unicorn_categories_{groupId}";
public static string MonthlyReport(Guid groupId, int year, int month, ...) => $"unicorn_monthly_{groupId}_{year}_{month}_...";

// CacheTags.cs
public static string Group(Guid groupId) => $"tag:group:{groupId}";
public const string Categories = "tag:categories";
public const string Reports = "tag:reports";

// CacheDurations.cs
public static readonly TimeSpan Categories = TimeSpan.FromHours(1);
public static readonly TimeSpan MonthlyReport = TimeSpan.FromHours(1);

Tag Invalidation Strategy

Prefer specific tags over group-wide invalidation:

// Too aggressive - invalidates EVERYTHING for the group
public IReadOnlyList<string> TagsToInvalidate => [CacheTags.Group(GroupId)];

// Better - only invalidates relevant caches
public IReadOnlyList<string> TagsToInvalidate => [CacheTags.Categories];

// When multiple types affected
public IReadOnlyList<string> TagsToInvalidate => [CacheTags.BudgetAccounts, CacheTags.Reports];

When to Cache

Cache Don't Cache
List queries User-specific mutable data
Reference data (categories) Real-time data
Reports/aggregations Small/fast queries
Geocode lookups -

HybridCache Benefits

  • Stampede protection: 100 concurrent requests = 1 DB query
  • Tag-based invalidation: RemoveByTagAsync("reports") clears all report caches
  • L2 ready: Can add Redis for multi-instance deployments