Skip to content

Performance Optimization

Overview

This section covers performance characteristics of the Flaggy feature flag library and optimization techniques for high-throughput production environments.

Performance Architecture

Multi-Layer Caching Strategy

Flaggy implements a sophisticated multi-layer caching architecture:

  1. Application-Level Cache (MemoryCache or Redis)
  2. Configurable TTL (default: 5 minutes)
  3. Automatic cache invalidation on updates
  4. Thread-safe operations

  5. Provider-Level Storage

  6. Database-backed (MySQL, PostgreSQL, MS SQL)
  7. In-memory with JSON persistence
  8. Connection pooling support
// Cache flow: Service → Cache → Provider → Database
var flag = await _flagService.is_enabledAsync("my-flag");
// 1st call: Cache miss → Database read → Cache store
// 2nd call: Cache hit → Instant return (no DB query)

Performance Characteristics

Operation With Cache Hit With Cache Miss Notes
is_enabledAsync() ~0.1 ms ~5-15 ms Depends on DB latency
GetValueAsync() ~0.1 ms ~5-15 ms Includes type conversion
GetAllFlagsAsync() ~0.5 ms ~10-30 ms Bulk operation optimized
Cache Refresh N/A ~10-30 ms Background operation
CreateFlagAsync() ~5-15 ms ~5-15 ms Writes to DB + cache

Caching Strategies

Strategy 1: Memory Cache (Single Instance)

Best for single-server deployments or when consistency requirements are relaxed.

using Flaggy.Enums;
using Flaggy.Extensions;

builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cachingProvider: CachingProvider.Memory,
    cacheExpiration: TimeSpan.FromMinutes(5) // Tune based on needs
);

Pros: - Fast: In-process memory access (~100 μs) - No external dependencies - Low latency

Cons: - Not distributed: Each instance has its own cache - Cache inconsistency in multi-server setups - Memory usage scales with flag count

Best for: Single-instance apps, development, testing

Strategy 2: Redis Cache (Distributed)

Best for multi-server deployments requiring consistency across instances.

using Flaggy.Enums;
using Flaggy.Extensions;

builder.Services.AddFlaggy(
    provider: new MySqlFeatureFlagProvider("..."),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: "localhost:6379,password=secret",
    cacheExpiration: TimeSpan.FromMinutes(10)
);

Pros: - Distributed: Shared cache across all instances - Consistent: Flag changes visible to all servers - Pub/Sub support for real-time updates

Cons: - Network latency: ~1-2 ms per call - External dependency - Additional infrastructure

Best for: Multi-server production deployments, microservices

Strategy 3: Hybrid Local + Distributed Cache

Combine local memory cache with Redis for best performance and consistency.

public class HybridFeatureFlagCache : IFeatureFlagCache
{
    private readonly IMemoryCache _localCache;
    private readonly IFeatureFlagCache _distributedCache;
    private readonly TimeSpan _localCacheExpiration = TimeSpan.FromSeconds(30);

    public async Task<FeatureFlag?> GetAsync(string key, CancellationToken ct = default)
    {
        // L1: Check local cache first (fast)
        if (_localCache.TryGetValue(key, out FeatureFlag? flag))
            return flag;

        // L2: Check distributed cache (slower but consistent)
        flag = await _distributedCache.GetAsync(key, ct);
        if (flag != null)
        {
            // Store in local cache for subsequent calls
            _localCache.Set(key, flag, _localCacheExpiration);
        }

        return flag;
    }

    public async Task SetAsync(string key, FeatureFlag flag, CancellationToken ct = default)
    {
        // Update both caches
        _localCache.Set(key, flag, _localCacheExpiration);
        await _distributedCache.SetAsync(key, flag, ct);
    }

    public async Task RemoveAsync(string key, CancellationToken ct = default)
    {
        _localCache.Remove(key);
        await _distributedCache.RemoveAsync(key, ct);
    }

    public async Task ClearAsync(CancellationToken ct = default)
    {
        _localCache.Clear();
        await _distributedCache.ClearAsync(ct);
    }

    public Task<IEnumerable<FeatureFlag>> GetAllAsync(CancellationToken ct = default)
    {
        return _distributedCache.GetAllAsync(ct);
    }

    public Task SetAllAsync(IEnumerable<FeatureFlag> flags, CancellationToken ct = default)
    {
        return _distributedCache.SetAllAsync(flags, ct);
    }
}

Cache Tuning

Optimal Cache Expiration Times

// Development: Short TTL for rapid iteration
builder.Services.AddFlaggy(
    provider: new InMemoryFeatureFlagProvider(),
    cacheExpiration: TimeSpan.FromSeconds(30)
);

// Production (low-change rate): Longer TTL for performance
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: connectionString
    );
    options.UseMemoryCache(TimeSpan.FromMinutes(15);
})
);

// Production (high-change rate): Shorter TTL for responsiveness
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: connectionString
    );
    options.UseMemoryCache(TimeSpan.FromMinutes(2);
})
);

Manual Cache Invalidation

For critical flags that need immediate propagation:

public class CriticalFlagService
{
    private readonly IFeatureFlagService _flagService;

    public async Task DisableFeatureImmediatelyAsync(string key)
    {
        await _flagService.DisableFlagAsync(key);
        // Force cache refresh across all instances
        await _flagService.RefreshCacheAsync();
    }
}

Warm-Up Cache on Startup

Pre-load flags during application startup:

var app = builder.Build();

// Warm up cache before serving traffic
using (var scope = app.Services.CreateScope())
{
    var flagService = scope.ServiceProvider.GetRequiredService<IFeatureFlagService>();
    await flagService.GetAllFlagsAsync(); // Populates cache
}

app.Run();

Database Performance

Connection Pooling

All database providers use connection pooling by default:

// MySQL
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: "Server=localhost;Database=myapp;User=root;Password=pass;Pooling=true;MinPoolSize=5;MaxPoolSize=50;"

    );
});

// PostgreSQL
builder.Services.AddFlaggy(options =>
{
    options.UsePostgreSQL(
        connectionString: "Host=localhost;Database=myapp;username=postgres;Password=pass;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"

    );
});

// MS SQL Server
builder.Services.AddFlaggy(options =>
{
    options.UseMsSql(
        connectionString: "Server=localhost;Database=myapp;User Id=sa;Password=pass;Pooling=true;Min Pool Size=5;Max Pool Size=50"

    );
});

Index Optimization

Flaggy's auto-migration system creates optimal indexes:

-- MySQL
CREATE TABLE feature_flags (
    `key` VARCHAR(255) PRIMARY KEY,  -- Clustered index on Key
    is_enabled BOOLEAN NOT NULL DEFAULT FALSE,
    `value` TEXT NULL,
    description TEXT NULL,
    created_at DATETIME NULL,
    updated_at DATETIME NULL
);

-- Additional index for queries filtering by is_enabled
CREATE INDEX idx_feature_flags_enabled ON feature_flags(is_enabled);

Query Optimization

// Bad - Multiple individual queries
foreach (var key in keys)
{
    var flag = await flagService.GetFlagAsync(key);
    // N+1 query problem
}

// Good - Single bulk query
var allFlags = await flagService.GetAllFlagsAsync();
var flagDict = allFlags.ToDictionary(f => f.Key);

foreach (var key in keys)
{
    if (flagDict.TryGetValue(key, out var flag))
    {
        // Use flag
    }
}

High-Throughput Patterns

Minimize Flag Checks in Hot Paths

// Bad - Checking flag on every request
app.MapGet("/api/products", async (IFeatureFlagService flagService) =>
{
    var useCache = await flagService.is_enabledAsync("use-product-cache");
    // Evaluated on every single request
});

// Good - Cache flag check result
public class ProductService
{
    private readonly IFeatureFlagService _flagService;
    private bool? _useCacheFlag;
    private DateTime _flagLastChecked = DateTime.MinValue;
    private readonly TimeSpan _recheckInterval = TimeSpan.FromSeconds(10);

    public async Task<List<Product>> GetProductsAsync()
    {
        // Re-check flag every 10 seconds instead of every request
        if (_useCacheFlag == null || DateTime.UtcNow - _flagLastChecked > _recheckInterval)
        {
            _useCacheFlag = await _flagService.is_enabledAsync("use-product-cache");
            _flagLastChecked = DateTime.UtcNow;
        }

        return _useCacheFlag.Value
            ? await GetProductsFromCache()
            : await GetProductsFromDatabase();
    }
}

Lazy Flag Loading

public class LazyFeatureFlagService
{
    private readonly IFeatureFlagService _flagService;
    private readonly ConcurrentDictionary<string, Lazy<Task<bool>>> _lazyFlags = new();

    public Task<bool> is_enabledAsync(string key)
    {
        var lazy = _lazyFlags.GetOrAdd(key, k => new Lazy<Task<bool>>(
            () => _flagService.is_enabledAsync(k)
        ));

        return lazy.Value;
    }

    public void Invalidate(string key)
    {
        _lazyFlags.TryRemove(key, out _);
    }
}

Batch Flag Evaluation

public class BatchFlagService
{
    private readonly IFeatureFlagService _flagService;

    public async Task<Dictionary<string, bool>> GetMultipleFlagsAsync(params string[] keys)
    {
        var allFlags = await _flagService.GetAllFlagsAsync();
        var flagDict = allFlags.ToDictionary(f => f.Key, f => f.is_enabled);

        return keys.ToDictionary(
            key => key,
            key => flagDict.TryGetValue(key, out var enabled) && enabled
        );
    }
}

// Usage
var flags = await batchService.GetMultipleFlagsAsync(
    "feature-1",
    "feature-2",
    "feature-3"
);

if (flags["feature-1"]) { /* ... */ }
if (flags["feature-2"]) { /* ... */ }

Memory Optimization

Minimize Flag Value Size

// Bad - Storing large JSON in flag value
await flagService.CreateFlagAsync(new FeatureFlag
{
    Key = "config",
    Value = JsonSerializer.Serialize(largeConfigObject) // Multiple KB
});

// Good - Store reference, fetch details from proper storage
await flagService.CreateFlagAsync(new FeatureFlag
{
    Key = "config-version",
    Value = "v2.1" // Small reference
});
// Fetch actual config from configuration system or database

Clean Up Unused Flags

public class FlagMaintenanceService
{
    private readonly IFeatureFlagService _flagService;
    private readonly ILogger<FlagMaintenanceService> _logger;

    public async Task ArchiveOldFlagsAsync()
    {
        var flags = await _flagService.GetAllFlagsAsync();
        var cutoffDate = DateTime.UtcNow.AddDays(-90);

        var flagsToArchive = flags.Where(f =>
            !f.is_enabled &&
            f.updated_at.HasValue &&
            f.updated_at.Value < cutoffDate
        ).ToList();

        _logger.LogInformation("Archiving {Count} old flags", flagsToArchive.Count);

        // Optional: Export to archive before deleting
        await ExportToArchive(flagsToArchive);

        // Delete from active system
        foreach (var flag in flagsToArchive)
        {
            await _flagService.DeleteFlagAsync(flag.Key);
        }
    }
}

Monitoring and Metrics

Performance Monitoring

public class InstrumentedFeatureFlagService : IFeatureFlagService
{
    private readonly IFeatureFlagService _inner;
    private readonly IMetrics _metrics;

    public async Task<bool> is_enabledAsync(string key, CancellationToken ct = default)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            var result = await _inner.is_enabledAsync(key, ct);
            _metrics.Histogram("feature_flag.check.duration_ms", sw.ElapsedMilliseconds);
            _metrics.Increment("feature_flag.check.success");
            return result;
        }
        catch (Exception ex)
        {
            _metrics.Increment("feature_flag.check.error");
            throw;
        }
    }

    // Implement other IFeatureFlagService methods...
}

Cache Hit Rate Monitoring

public class CacheMonitoringService
{
    private long _cacheHits = 0;
    private long _cacheMisses = 0;

    public void RecordCacheHit() => Interlocked.Increment(ref _cacheHits);
    public void RecordCacheMiss() => Interlocked.Increment(ref _cacheMisses);

    public double GetHitRate()
    {
        var hits = _cacheHits;
        var misses = _cacheMisses;
        var total = hits + misses;
        return total == 0 ? 0 : (double)hits / total * 100;
    }
}

Benchmarks

Performance Comparison

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class FeatureFlagBenchmarks
{
    private IFeatureFlagService _memoryCacheService;
    private IFeatureFlagService _redisCacheService;

    [GlobalSetup]
    public void Setup()
    {
        // Setup services
    }

    [Benchmark]
    public async Task<bool> MemoryCache_is_enabled()
    {
        return await _memoryCacheService.is_enabledAsync("test-flag");
    }

    [Benchmark]
    public async Task<bool> RedisCache_is_enabled()
    {
        return await _redisCacheService.is_enabledAsync("test-flag");
    }

    [Benchmark]
    public async Task<int?> GetIntValue_WithConversion()
    {
        return await _memoryCacheService.GetValueAsync<int>("test-flag", defaultValue: 0);
    }
}

// Typical Results:
// MemoryCache_is_enabled:        ~100 μs (cache hit)
// RedisCache_is_enabled:         ~1-2 ms (network + cache hit)
// GetIntValue_WithConversion:   ~120 μs (includes type conversion)

Production Configuration

// High-traffic production API
builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: builder.Configuration.GetConnectionString("Flaggy"
    );
}),
    tableName: "feature_flags",
    cacheExpiration: TimeSpan.FromMinutes(5),  // Balance between freshness and performance
    autoMigrate: false  // Run migrations manually in production
);

// Configure Redis for distributed caching
builder.Services.AddFlaggy(
    provider: sp.GetRequiredService<IFeatureFlagProvider>(),
    cachingProvider: CachingProvider.Redis,
    redisConnectionString: builder.Configuration["Redis:ConnectionString"],
    cacheExpiration: TimeSpan.FromMinutes(5)
);

// Configure health checks
builder.Services.AddHealthChecks()
    .AddCheck<FeatureFlagHealthCheck>("feature_flags");

Health Check Implementation

public class FeatureFlagHealthCheck : IHealthCheck
{
    private readonly IFeatureFlagService _flagService;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        try
        {
            var sw = Stopwatch.StartNew();
            await _flagService.GetAllFlagsAsync(ct);
            sw.Stop();

            if (sw.ElapsedMilliseconds > 1000)
            {
                return HealthCheckResult.Degraded(
                    $"Feature flag service is slow: {sw.ElapsedMilliseconds}ms"
                );
            }

            return HealthCheckResult.Healthy(
                $"Feature flag service is healthy ({sw.ElapsedMilliseconds}ms)"
            );
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Feature flag service is unavailable",
                ex
            );
        }
    }
}

Next Steps