Skip to content

Caching

Flaggy provides flexible and efficient caching strategies to optimize performance and reduce database load. It supports both in-memory caching (using IMemoryCache) and distributed caching (using Redis) for scalable multi-instance deployments.

Overview

Caching in Flaggy offers several key benefits:

  • Performance: Reduces database queries by caching frequently accessed flags
  • Scalability: Redis support for distributed scenarios
  • Automatic Invalidation: Cache is automatically cleared when flags are modified
  • Configurable Expiration: Set custom cache duration based on your needs
  • Thread-Safe: All cache operations are thread-safe
  • Manual Refresh: Force cache reload when needed

Memory Cache (Default)

Memory cache is the default caching strategy, using ASP.NET Core's built-in IMemoryCache. It's perfect for single-instance applications and provides excellent performance.

Features

  • Built into ASP.NET Core (no additional packages)
  • Extremely fast read/write operations
  • Automatic memory management
  • Per-instance caching (not shared across servers)
  • Perfect for development and single-instance production

Basic Configuration

using Flaggy.Extensions;
using Flaggy.Providers;

var builder = WebApplication.CreateBuilder(args);

// Default: MemoryCache with 5 minutes expiration
builder.Services.AddFlaggy(new InMemoryFeatureFlagProvider());

var app = builder.Build();
app.Run();

Custom Cache Expiration

Configure a custom expiration time for cached flags:

using Flaggy.Enums;
using Flaggy.Extensions;
using Flaggy.Providers;

// Cache for 10 minutes
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Memory,
    cacheExpiration: TimeSpan.FromMinutes(10)
);

// Cache for 30 seconds (frequent updates)
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Memory,
    cacheExpiration: TimeSpan.FromSeconds(30)
);

// Cache for 1 hour (infrequent updates)
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Memory,
    cacheExpiration: TimeSpan.FromHours(1)
);

With Database Providers

Memory cache works with all database providers:

using Flaggy.Extensions;
using Flaggy.Enums;

// MySQL with MemoryCache (default)
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;"
    );
    options.UseMemoryCache(TimeSpan.FromMinutes(5);
})
);

// PostgreSQL with MemoryCache
builder.Services.AddFlaggy(options =>
{
    options.UsePostgreSQL(
        connectionString: "Host=localhost;Database=myapp;username=postgres;Password=pass"
    );
    options.UseMemoryCache(TimeSpan.FromMinutes(5);
})
);

// MS SQL with MemoryCache
builder.Services.AddFlaggy(options =>
{
    options.UseMsSql(
        connectionString: "Server=localhost;Database=myapp;User Id=sa;Password=pass;TrustServerCertificate=True"
    );
    options.UseMemoryCache(TimeSpan.FromMinutes(5);
})
);

Redis Cache

Redis cache enables distributed caching for multi-instance deployments. Perfect for load-balanced applications and microservices architectures.

Features

  • Distributed caching across multiple instances
  • Shared cache for all application servers
  • Supports Redis 6+ authentication
  • SSL/TLS support
  • Configurable database selection
  • Automatic serialization/deserialization
  • Batch operations for performance

Installation

dotnet add package Flaggy.Caching.Redis

Basic Configuration

using Flaggy.Enums;
using Flaggy.Extensions;
using Flaggy.Providers;

var builder = WebApplication.CreateBuilder(args);

// Simple Redis configuration (no authentication)
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379",
    cacheExpiration: TimeSpan.FromMinutes(10)
);

var app = builder.Build();
app.Run();

Redis Connection Strings

Flaggy supports various Redis connection string formats:

No Authentication

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379"
);

Password Authentication

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379,password=mypassword"
);

username and Password (Redis 6+)

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379,user=myuser,password=mypassword"
);

SSL/TLS Connection

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6380,ssl=true,password=mypassword"
);

Multiple Hosts (Cluster)

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "server1:6379,server2:6379,password=mypassword"
);

Custom Database

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379,password=mypassword",
    redisDatabase: 2  // Use database 2 instead of default (0)
);

With Database Providers

Redis cache works seamlessly with all database providers:

using Flaggy.Extensions;
using Flaggy.Enums;
using Microsoft.Extensions.DependencyInjection;

// MySQL with Redis cache
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;"

    );
});

builder.Services.AddFlaggy(
    provider: sp.GetRequiredService<IFeatureFlagProvider>(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379,password=mypassword",
    cacheExpiration: TimeSpan.FromMinutes(10)
);

Redis Key Structure

Flaggy uses a consistent key naming pattern in Redis:

  • Individual flags: flaggy:flag:{key}
  • All flags collection: flaggy:all-flags
  • Key set tracking: flaggy:keys

Example Redis keys:

flaggy:flag:new-feature
flaggy:flag:beta-mode
flaggy:flag:dark-theme
flaggy:all-flags
flaggy:keys

Redis Configuration Best Practices

From Configuration File

// appsettings.json
{
  "Redis": {
    "ConnectionString": "localhost:6379,password=mypassword",
    "Database": 0
  },
  "Flaggy": {
    "CacheExpiration": "00:10:00"
  }
}

// Program.cs
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: builder.Configuration["Redis:ConnectionString"],
    redisDatabase: builder.Configuration.GetValue<int>("Redis:Database"),
    cacheExpiration: builder.Configuration.GetValue<TimeSpan>("Flaggy:CacheExpiration")
);

Environment-Specific Configuration

var redisConnectionString = builder.Environment.IsDevelopment()
    ? "localhost:6379"  // Local Redis for development
    : builder.Configuration.GetConnectionString("Redis");  // Production Redis

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: redisConnectionString
);

Cache Behavior

Automatic Invalidation

The cache is automatically invalidated when flags are modified:

public class MyService
{
    private readonly IFeatureFlagService _flagService;

    public MyService(IFeatureFlagService flagService)
    {
        _flagService = flagService;
    }

    public async Task UpdateFeatureFlag()
    {
        // Get flag (reads from cache)
        var flag = await _flagService.GetFlagAsync("new-feature");

        // Update flag
        flag.is_enabled = true;
        await _flagService.UpdateFlagAsync(flag);

        // Cache is automatically invalidated
        // Next read will fetch from database and update cache

        // This will get the updated value
        var updatedFlag = await _flagService.GetFlagAsync("new-feature");
    }
}

Cache Operations

All CRUD operations trigger appropriate cache updates:

Operation Cache Behavior
CreateFlagAsync() Adds to cache, invalidates all-flags cache
UpdateFlagAsync() Updates cache, invalidates all-flags cache
DeleteFlagAsync() Removes from cache, invalidates all-flags cache
GetFlagAsync() Reads from cache, falls back to database
GetAllFlagsAsync() Reads from all-flags cache, falls back to database
RefreshCacheAsync() Clears and reloads all flags

Manual Cache Refresh

Force a cache refresh when needed:

app.MapPost("/api/flags/refresh", async (IFeatureFlagService flagService) =>
{
    await flagService.RefreshCacheAsync();
    return Results.Ok(new { message = "Cache refreshed successfully" });
});

Cache Expiration Strategy

Flaggy uses a combination of absolute and sliding expiration:

// Absolute expiration: Cache expires after configured time
// Example: 5 minutes after creation
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cacheExpiration: TimeSpan.FromMinutes(5)
);

The cache refresh logic works as follows:

  1. First request loads flags from database into cache
  2. Subsequent requests read from cache (fast)
  3. After expiration time, cache is automatically refreshed on next request
  4. CRUD operations immediately update cache

Performance Optimization

1. Choose the Right Cache Duration

// Frequently changing flags (e.g., A/B tests)
cacheExpiration: TimeSpan.FromSeconds(30)

// Normal flags (default)
cacheExpiration: TimeSpan.FromMinutes(5)

// Rarely changing flags
cacheExpiration: TimeSpan.FromMinutes(30)

// Static flags (rarely updated in production)
cacheExpiration: TimeSpan.FromHours(1)

2. Use GetAllFlagsAsync() for Batch Operations

// Efficient: Single cache lookup
var allFlags = await flagService.GetAllFlagsAsync();
var enabledFlags = allFlags.Where(f => f.is_enabled).ToList();

// Less efficient: Multiple cache lookups
var flag1 = await flagService.GetFlagAsync("feature-1");
var flag2 = await flagService.GetFlagAsync("feature-2");
var flag3 = await flagService.GetFlagAsync("feature-3");

3. Preload Cache at Startup

var app = builder.Build();

// Warm up the cache
using (var scope = app.Services.CreateScope())
{
    var flagService = scope.ServiceProvider.GetRequiredService<IFeatureFlagService>();
    await flagService.RefreshCacheAsync();
}

app.Run();

4. Monitor Cache Performance

app.MapGet("/api/cache/stats", async (IFeatureFlagService flagService) =>
{
    var flags = await flagService.GetAllFlagsAsync();
    var summary = await flagService.GetFlagsSummaryAsync();

    return Results.Ok(new
    {
        TotalFlags = summary.TotalCount,
        EnabledFlags = summary.EnabledCount,
        DisabledFlags = summary.DisabledCount,
        CachedAt = DateTime.UtcNow
    });
});

Distributed Caching Scenarios

Load-Balanced Web Application

┌─────────────┐
│ Load        │
│ Balancer    │
└──────┬──────┘
       │
       ├─────────┬─────────┬─────────┐
       │         │         │         │
   ┌───▼───┐ ┌──▼────┐ ┌──▼────┐ ┌──▼────┐
   │App    │ │App    │ │App    │ │App    │
   │Server │ │Server │ │Server │ │Server │
   │1      │ │2      │ │3      │ │4      │
   └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘
       │         │         │         │
       └─────────┴────┬────┴─────────┘
                      │
              ┌───────▼────────┐
              │                │
              │  Redis Cache   │
              │  (Shared)      │
              │                │
              └───────┬────────┘
                      │
              ┌───────▼────────┐
              │                │
              │   Database     │
              │   (MySQL/      │
              │   PostgreSQL)  │
              │                │
              └────────────────┘

Use Redis cache for this scenario to ensure all servers see consistent flags.

Microservices Architecture

┌──────────┐     ┌──────────┐     ┌──────────┐
│Service A │     │Service B │     │Service C │
│(Flaggy)  │     │(Flaggy)  │     │(Flaggy)  │
└────┬─────┘     └────┬─────┘     └────┬─────┘
     │                │                │
     └────────────────┼────────────────┘
                      │
              ┌───────▼────────┐
              │  Redis Cache   │
              │  (Shared)      │
              └───────┬────────┘
                      │
              ┌───────▼────────┐
              │   Database     │
              └────────────────┘

Each microservice can share the same Redis cache for consistent feature flags across services.

Troubleshooting

Cache Not Updating

Problem: Changes made through dashboard or API are not reflected.

Solution: Ensure cache is properly configured and invalidation is working:

// Verify cache invalidation after update
var flag = await flagService.GetFlagAsync("test-flag");
flag.is_enabled = true;
var updated = await flagService.UpdateFlagAsync(flag);
if (updated)
{
    // Force refresh if needed
    await flagService.RefreshCacheAsync();
}

Redis Connection Issues

Problem: Cannot connect to Redis server.

Solution: Check connection string and Redis server status:

// Test Redis connection
using StackExchange.Redis;

try
{
    var connection = ConnectionMultiplexer.Connect("localhost:6379,password=mypassword");
    var db = connection.GetDatabase();
    await db.PingAsync();
    Console.WriteLine("Redis connection successful");
}
catch (Exception ex)
{
    Console.WriteLine($"Redis connection failed: {ex.Message}");
}

High Memory Usage (MemoryCache)

Problem: Application consuming too much memory.

Solution: Reduce cache expiration time:

// Shorter cache duration
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cacheExpiration: TimeSpan.FromMinutes(1)
);

Stale Cache Data

Problem: Cache shows old data after database changes.

Solution: Use manual refresh or reduce cache expiration:

// Reduce cache expiration
cacheExpiration: TimeSpan.FromSeconds(30)

// Or force refresh after external changes
await flagService.RefreshCacheAsync();

Best Practices

1. Choose the Right Caching Strategy

  • Single instance app: Use MemoryCache (default)
  • Load-balanced app: Use Redis
  • Microservices: Use Redis with shared connection
  • Development: Use MemoryCache for simplicity

2. Set Appropriate Cache Expiration

  • Frequently changing flags: 30 seconds - 1 minute
  • Normal operations: 5 minutes (default)
  • Stable production flags: 15-30 minutes
  • Static configuration flags: 1 hour

3. Handle Cache Failures Gracefully

Flaggy automatically falls back to the database if cache fails:

public async Task<FeatureFlag?> GetFlagAsync(string key)
{
    // Try cache first
    var cachedFlag = await _cache.GetAsync(key);
    if (cachedFlag != null)
    {
        return cachedFlag;
    }

    // Fall back to database
    var flag = await _provider.GetFlagAsync(key);
    if (flag != null)
    {
        await _cache.SetAsync(key, flag);
    }
    return flag;
}

4. Monitor Cache Hit Rate

// Track cache performance
var stopwatch = Stopwatch.StartNew();
var flag = await flagService.GetFlagAsync("test-flag");
stopwatch.Stop();

// Log slow queries (cache miss)
if (stopwatch.ElapsedMilliseconds > 10)
{
    _logger.LogWarning("Slow flag retrieval: {Key} took {Ms}ms", "test-flag", stopwatch.ElapsedMilliseconds);
}

5. Use Separate Redis Databases

// Use different databases for different environments
var redisDatabase = builder.Environment.IsDevelopment() ? 0 : 1;

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379",
    redisDatabase: redisDatabase
);