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"
});