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:
- First request loads flags from database into cache
- Subsequent requests read from cache (fast)
- After expiration time, cache is automatically refreshed on next request
- 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
);
Related Topics¶
- Providers - Configure storage providers
- Programmatic API - CRUD operations and cache invalidation
- Dashboard - Managing flags through the web UI
- Flag Values - Working with typed flag values