Skip to content

Best Practices

Overview

This section contains recommended best practices for using the Flaggy feature flag library in production environments.

Flag Naming Conventions

Use Consistent Naming

// Bad - Inconsistent naming styles
await flagService.is_enabledAsync("NewFeature");
await flagService.is_enabledAsync("beta_mode");
await flagService.is_enabledAsync("DARK-MODE");

// Good - Consistent kebab-case naming
await flagService.is_enabledAsync("new-feature");
await flagService.is_enabledAsync("beta-mode");
await flagService.is_enabledAsync("dark-mode");

Use Descriptive Names

// Bad - Vague naming
await flagService.is_enabledAsync("feature1");
await flagService.is_enabledAsync("temp");

// Good - Clear, descriptive names
await flagService.is_enabledAsync("customer-dashboard-v2");
await flagService.is_enabledAsync("payment-processor-stripe");
await flagService.is_enabledAsync("experimental-search-algorithm");

Organize with Prefixes

// Group related flags with prefixes
await flagService.is_enabledAsync("ui-dark-mode");
await flagService.is_enabledAsync("ui-new-header");
await flagService.is_enabledAsync("api-rate-limiting");
await flagService.is_enabledAsync("api-circuit-breaker");
await flagService.is_enabledAsync("feature-premium-analytics");
await flagService.is_enabledAsync("feature-export-to-pdf");

Flag Management

Always Provide descriptions

// Bad - No description
await flagService.CreateFlagAsync(new FeatureFlag
{
    Key = "new-feature",
    is_enabled = false
});

// Good - Clear description with context
await flagService.CreateFlagAsync(new FeatureFlag
{
    Key = "new-feature",
    is_enabled = false,
    description = "New product recommendation engine using ML - rolled out to 10% of users starting 2025-01-15"
});

Use Default Values for Safety

// Bad - No default value, potential null reference
var maxRetries = await flagService.GetValueAsync<int>("max-retries");
if (maxRetries.HasValue)
{
    for (int i = 0; i < maxRetries.Value; i++) { /* ... */ }
}

// Good - Safe default value
var maxRetries = await flagService.GetValueAsync<int>("max-retries", defaultValue: 3);
for (int i = 0; i < maxRetries.Value; i++)
{
    // Safely execute with default fallback
}

Initialize Flags at Startup

// Seed critical flags at application startup
public static async Task Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddFlaggy(options =>
{
    options.UseMySQL(
        connectionString: "Server=localhost;Database=myapp;..."
    );
});

    var app = builder.Build();

    // Initialize critical flags
    await app.InitializeFeatureFlagsAsync(async flagService =>
    {
        await flagService.CreateFlagIfNotExistsAsync(
            "maintenance-mode",
            isEnabled: false,
            description: "Emergency maintenance mode - disables all user access"
        );

        await flagService.CreateFlagIfNotExistsAsync(
            "rate-limit-requests",
            isEnabled: true,
            value: "100",
            description: "Max API requests per minute per user"
        );
    });

    app.Run();
}

Flag Lifecycle Management

Use Progressive Rollouts

public class FeatureRolloutService
{
    private readonly IFeatureFlagService _flagService;

    // Stage 1: Internal testing
    public async Task EnableForInternalAsync()
    {
        await _flagService.UpsertFlagAsync(
            "new-checkout-flow",
            isEnabled: true,
            value: "internal-only",
            description: "Phase 1: Internal testing only"
        );
    }

    // Stage 2: Beta users (10%)
    public async Task EnableForBetaAsync()
    {
        await _flagService.UpdateFlagValueAsync(
            "new-checkout-flow",
            value: "beta-10-percent"
        );
        await _flagService.UpdateFlagdescriptionAsync(
            "new-checkout-flow",
            "Phase 2: Beta users - 10% rollout"
        );
    }

    // Stage 3: Full rollout
    public async Task EnableForAllAsync()
    {
        await _flagService.UpdateFlagValueAsync(
            "new-checkout-flow",
            value: "all-users"
        );
        await _flagService.UpdateFlagdescriptionAsync(
            "new-checkout-flow",
            "Phase 3: Full production rollout"
        );
    }
}

Clean Up Old Flags

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

    public async Task CleanupOldFlagsAsync()
    {
        var allFlags = await _flagService.GetAllFlagsAsync();
        var now = DateTime.UtcNow;

        foreach (var flag in allFlags)
        {
            // Remove flags older than 90 days that are disabled
            if (!flag.is_enabled &&
                flag.updated_at.HasValue &&
                (now - flag.updated_at.Value).TotalDays > 90)
            {
                _logger.LogInformation(
                    "Removing stale flag: {Key} (last updated: {updated_at})",
                    flag.Key, flag.updated_at
                );
                await _flagService.DeleteFlagAsync(flag.Key);
            }
        }
    }
}

Error Handling and Resilience

Always Handle Flag Failures Gracefully

// Bad - No error handling
public async Task<IActionResult> GetProducts()
{
    var useNewApi = await _flagService.is_enabledAsync("use-new-product-api");
    // Will fail if flag service is down
}

// Good - Defensive programming with fallback
public async Task<IActionResult> GetProducts()
{
    bool useNewApi;
    try
    {
        useNewApi = await _flagService.is_enabledAsync("use-new-product-api");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Failed to check feature flag, using default");
        useNewApi = false; // Safe default
    }

    return useNewApi ? await GetProductsFromNewApi() : await GetProductsFromLegacyApi();
}

Use Circuit Breaker Pattern for Critical Paths

public class ResilientFeatureFlagService
{
    private readonly IFeatureFlagService _flagService;
    private readonly ILogger<ResilientFeatureFlagService> _logger;
    private readonly ConcurrentDictionary<string, (bool value, DateTime expiry)> _localCache = new();

    public async Task<bool> is_enabledWithFallbackAsync(string key, bool defaultValue = false)
    {
        // Try cache first
        if (_localCache.TryGetValue(key, out var cached) && cached.expiry > DateTime.UtcNow)
        {
            return cached.value;
        }

        try
        {
            var isEnabled = await _flagService.is_enabledAsync(key);
            // Cache for 30 seconds as fallback
            _localCache[key] = (isEnabled, DateTime.UtcNow.AddSeconds(30));
            return isEnabled;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to fetch flag {Key}, using default: {Default}", key, defaultValue);
            return defaultValue;
        }
    }
}

Performance Best Practices

Minimize Flag Checks in Hot Paths

// Bad - Checking flag inside tight loop
public async Task ProcessOrders(List<Order> orders)
{
    foreach (var order in orders)
    {
        var useNewProcessor = await _flagService.is_enabledAsync("new-order-processor");
        // Checking flag thousands of times
    }
}

// Good - Check flag once before loop
public async Task ProcessOrders(List<Order> orders)
{
    var useNewProcessor = await _flagService.is_enabledAsync("new-order-processor");

    foreach (var order in orders)
    {
        if (useNewProcessor)
            await ProcessWithNewEngine(order);
        else
            await ProcessWithLegacyEngine(order);
    }
}

Cache Flags in Memory for High-Frequency Access

public class FeatureFlagCache
{
    private readonly IFeatureFlagService _flagService;
    private readonly IMemoryCache _memoryCache;

    public async Task<bool> is_enabledAsync(string key)
    {
        return await _memoryCache.GetOrCreateAsync($"flag_{key}", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
            return await _flagService.is_enabledAsync(key);
        });
    }
}

Use Batch Operations When Possible

// Bad - Multiple individual operations
await flagService.CreateFlagAsync(flag1);
await flagService.CreateFlagAsync(flag2);
await flagService.CreateFlagAsync(flag3);

// Good - Use extension methods for batch operations
await flagService.SeedFlagsAsync(new[] { flag1, flag2, flag3 });

Security Best Practices

Never Store Sensitive Data in Flags

// Bad - Storing secrets in flags
await flagService.CreateFlagAsync(new FeatureFlag
{
    Key = "api-key",
    Value = "sk_live_12345", // NEVER DO THIS
    is_enabled = true
});

// Good - Store configuration type, not secrets
await flagService.CreateFlagAsync(new FeatureFlag
{
    Key = "payment-provider",
    Value = "stripe", // Just the provider name
    is_enabled = true,
    description = "Payment provider to use (credentials in secure vault)"
});

Protect Dashboard Access

// Always require authorization for production
app.UseFlaggyUI(options =>
{
    options.RoutePrefix = "/admin/feature-flags";
    options.RequireAuthorization = true;
    options.AuthorizationFilter = context =>
    {
        // Restrict to admin users only
        return context.User.IsInRole("Admin") ||
               context.User.IsInRole("FeatureFlagManager");
    };
});

Audit Flag Changes

public class AuditedFeatureFlagService : IFeatureFlagService
{
    private readonly IFeatureFlagService _inner;
    private readonly IAuditLogger _auditLogger;

    public async Task<bool> UpdateFlagAsync(FeatureFlag flag, CancellationToken ct = default)
    {
        var before = await _inner.GetFlagAsync(flag.Key, ct);
        var result = await _inner.UpdateFlagAsync(flag, ct);

        if (result)
        {
            await _auditLogger.LogAsync(new AuditEntry
            {
                Action = "UpdateFeatureFlag",
                FlagKey = flag.Key,
                Before = before,
                After = flag,
                Timestamp = DateTime.UtcNow,
                User = GetCurrentUser()
            });
        }

        return result;
    }
}

Testing Best Practices

Use InMemory Provider for Tests

public class FeatureFlagTests
{
    private readonly IFeatureFlagService _flagService;

    public FeatureFlagTests()
    {
        var services = new ServiceCollection();
        services.AddFlaggy(new InMemoryFeatureFlagProvider());
        var provider = services.BuildServiceProvider();
        _flagService = provider.GetRequiredService<IFeatureFlagService>();
    }

    [Fact]
    public async Task WhenFlagEnabled_ShouldUseNewFeature()
    {
        // Arrange
        await _flagService.CreateFlagAsync(new FeatureFlag
        {
            Key = "new-feature",
            is_enabled = true
        });

        // Act
        var result = await _flagService.is_enabledAsync("new-feature");

        // Assert
        result.Should().BeTrue();
    }
}

Test Both Flag States

[Theory]
[InlineData(true, "new-algorithm")]
[InlineData(false, "legacy-algorithm")]
public async Task ProcessData_ShouldUseCorrectAlgorithm(bool flagEnabled, string expectedAlgorithm)
{
    // Arrange
    await _flagService.UpsertFlagAsync("use-new-algorithm", flagEnabled);

    // Act
    var result = await _processor.ProcessData(testData);

    // Assert
    result.AlgorithmUsed.Should().Be(expectedAlgorithm);
}

Monitoring and Observability

Log Flag Evaluations

public async Task<IActionResult> ProcessPayment()
{
    var useNewProcessor = await _flagService.is_enabledAsync("new-payment-processor");

    _logger.LogInformation(
        "Payment processing: using {Processor} (flag: new-payment-processor={Enabled})",
        useNewProcessor ? "New" : "Legacy",
        useNewProcessor
    );

    return useNewProcessor
        ? await ProcessWithNewEngine()
        : await ProcessWithLegacyEngine();
}

Track Flag Usage Metrics

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

    public async Task<bool> is_enabledAsync(string key, CancellationToken ct = default)
    {
        var isEnabled = await _inner.is_enabledAsync(key, ct);

        _metrics.Increment("feature_flag.check", new Dictionary<string, string>
        {
            { "flag_key", key },
            { "enabled", isEnabled.ToString() }
        });

        return isEnabled;
    }
}

Documentation

Document Flag Purpose and Lifecycle

// Create comprehensive flag documentation
await flagService.CreateFlagAsync(new FeatureFlag
{
    Key = "checkout-optimization-experiment",
    is_enabled = true,
    Value = "variant-b",
    description = @"A/B test for checkout flow optimization
        - Owner: payments-team@company.com
        - Jira: PAY-1234
        - Start Date: 2025-01-15
        - Expected End Date: 2025-02-15
        - Rollback Plan: Set is_enabled=false
        - Success Criteria: 5% increase in conversion"
});

Next Steps