Feature Flags
Key Points
- Feature flags decouple deploy from release. Code ships; flag stays off; flip on at runtime when ready.
Microsoft.FeatureManagementis the .NET library. Azure App Configuration is the canonical store. Alternatives: LaunchDarkly, GrowthBook, Unleash.- Patterns: kill switches, percentage rollouts, targeted rollouts (specific users/groups), A/B variants, environment gates.
- Hygiene: every flag has an owner + cleanup date. Stale flags = tech debt + complexity.
- Don't: gate critical security in flags; use flags for permanent config (use config instead).
Concepts (deep dive)
Why feature flags exist
Without flags, deploy = release: the moment your code reaches production, every user has the new behavior. That coupling forces every interesting release to also be a high-risk one — you can't ship code without simultaneously committing to user impact, and you can't roll back without redeploying. Feature flags decouple the two: the new code ships behind an off switch, and you flip the switch later when you're confident. Now you can:
- Dark-launch code paths for weeks before exposing them, so production traffic warms caches and surfaces bugs before any user sees them.
- Roll out to 1% / 10% / 100% of users, watching dashboards, with one click rollback (flip flag → off).
- Target specific users (your team, beta cohort, enterprise customer) before general availability.
- A/B test by serving variants and comparing metrics.
- Kill switch broken features instantly without deploying a hotfix.
without flags with flags
───────────── ──────────
deploy ──► every user gets it deploy ──► nobody gets it yet
bug? ──► roll back deploy │ (code is dark)
(slow, blast radius) ▼
flip flag 1% ─► metrics ok? ▼
flip 10%
flip 100%
bug? ─► flip back to 0% (instant)
The price is complexity in the codebase: every flag is a branch that has to be tested, maintained, eventually removed. The hygiene rules below exist because un-cleaned flags accumulate into "what does this flag do?" archeology nobody wants to do.
Setup
builder.Configuration.AddAzureAppConfiguration(o => o.UseFeatureFlags());
builder.Services.AddFeatureManagement();
public class C(IFeatureManager fm)
{
public async Task M()
{
if (await fm.IsEnabledAsync("NewBilling"))
return /* new */;
return /* old */;
}
}
Flag types
Boolean (on/off)
Percentage rollout
{
"id": "NewBilling",
"enabled": true,
"conditions": {
"client_filters": [{
"name": "Microsoft.Percentage",
"parameters": { "Value": 25 }
}]
}
}
25% of evaluations return true. Hash-based per session/user.
Targeting (user + groups)
{
"client_filters": [{
"name": "Microsoft.Targeting",
"parameters": {
"Audience": {
"Users": ["[email protected]"],
"Groups": [{ "Name": "BetaTesters", "RolloutPercentage": 100 }],
"DefaultRolloutPercentage": 5
}
}
}]
}
public class TargetingContextAccessor(IHttpContextAccessor http) : ITargetingContextAccessor
{
public ValueTask<TargetingContext> GetContextAsync()
{
var user = http.HttpContext?.User;
return new(new TargetingContext
{
UserId = user?.FindFirstValue(ClaimTypes.NameIdentifier),
Groups = user?.FindAll(ClaimTypes.Role).Select(c => c.Value)
});
}
}
builder.Services.AddSingleton<ITargetingContextAccessor, TargetingContextAccessor>();
Time window
{
"client_filters": [{
"name": "Microsoft.TimeWindow",
"parameters": { "Start": "2026-04-01T00:00:00Z", "End": "2026-05-01T00:00:00Z" }
}]
}
Auto-enable / auto-disable.
Variants (A/B)
Different values per audience. Track conversion via OTel custom metrics.
Custom filters
[FilterAlias("Country")]
public class CountryFilter : IFeatureFilter
{
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext ctx)
{
var allowed = ctx.Parameters.GetSection("Countries").Get<string[]>();
var current = HttpContextAccessor.HttpContext?.Request.Headers["X-Country"].ToString();
return Task.FromResult(allowed?.Contains(current) ?? false);
}
}
builder.Services.AddFeatureManagement().AddFeatureFilter<CountryFilter>();
Per-request consistency
IFeatureManagerSnapshot ensures flag value is consistent within one request:
Patterns
Kill switches
Toggle off in seconds during incidents.
Progressive rollout
1% → 5% → 25% → 50% → 100% over days. Monitor metrics each step. Roll back via flag if issues.
Dark launch
Deploy code; flag off; canary on for internal users; observe; gradually enable.
Trunk-based development
Merge incomplete features behind flags. Continuous integration; deploy daily; release when ready.
Backends
| Backend | Notes |
|---|---|
| Azure App Configuration | Native MS; cheap; integrated with Identity |
| LaunchDarkly | Most features; mature; expensive |
| GrowthBook | Open-source; analytics integration |
| Unleash | Open-source; self-hosted |
| Optimizely | Marketing-leaning |
For .NET + Azure: App Configuration. For sophisticated experimentation: LaunchDarkly.
Refresh
Azure App Configuration polls every N seconds. Push refresh via Service Bus for instant.
Hygiene
- Owner: who manages this flag
- Created: date
- Cleanup target: date or condition
- Status: dev / rollout / stable / cleanup pending
After 100% rollout for 2 weeks → remove the flag and the dead code branch. Otherwise: tech debt accumulates.
Anti-patterns
- Permanent flags for what should be config (use IConfiguration).
- Flag for security: permission checks.
- Flag combinatorics: 5 flags = 32 code paths, hard to test.
- Flags forever: clean up.
- No metrics: rolling out blindly.
Testing
Unit-test both flag states. Integration test the on path; smoke-test the off path.
[Fact]
public async Task NewFeature_when_off_uses_old_path()
{
var fm = Substitute.For<IFeatureManager>();
fm.IsEnabledAsync("NewFeature").Returns(false);
var sut = new C(fm);
Assert.Equal(OldResult, await sut.M());
}
Metrics-driven rollout
if (await fm.IsEnabledAsync("NewBilling"))
{
_newBillingMetric.Add(1);
return NewBilling();
}
return OldBilling();
Track success/error rate per branch. Compare. Decide.
Server-side vs client-side
- Server-side: evaluate on backend; ship final result. Safer for sensitive logic.
- Client-side (browser): SPA evaluates flags. Lower latency for UI; flag values exposed.
For visual A/B: client-side. For business logic: server-side.
Code: correct vs wrong
❌ Wrong: env var-based "flag"
Requires redeploy to flip.
✅ Correct: managed flag
❌ Wrong: flag for auth
Auth must be enforced in policy/code, not toggled.
✅ Correct: flag for behavior, auth via [Authorize]
Design patterns for this topic
Pattern 1 — "Kill switch for incidents"
- Intent: disable bad feature in seconds.
Pattern 2 — "Percentage rollout"
- Intent: gradual launch.
Pattern 3 — "Targeting + groups"
- Intent: beta cohorts; gradual.
Pattern 4 — "Dark launch"
- Intent: deploy code; release later.
Pattern 5 — "Hygiene + cleanup dates"
- Intent: prevent debt.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Flags | Decouple deploy/release | Code complexity |
| App Config | Cheap; integrated | Polling latency |
| LaunchDarkly | Rich | Cost |
| Server-side | Secure | Round trip |
| Client-side | Fast UI | Exposes flags |
When to use / when to avoid
- Use for risky launches.
- Use for incident kill switches.
- Avoid for permanent config.
- Avoid for auth.
Interview Q&A
Q1. Why feature flags? Decouple deploy from release. Code ships; flag controls activation.
Q2. Built-in filters? Percentage, TimeWindow, Targeting.
Q3. Custom filter? Implement IFeatureFilter; register via AddFeatureFilter<T>.
Q4. Targeting context? ITargetingContextAccessor provides user/groups for targeting filter.
Q5. Variants vs boolean? Variants: multiple values (A/B). Boolean: on/off.
Q6. IFeatureManager vs Snapshot? Snapshot: consistent within one request. Manager: each call evaluated.
Q7. Anti-pattern: flag for auth? Yes — auth in policy. Flags for behavior.
Q8. Flag hygiene? Owner, created, cleanup date. Remove after stable rollout.
Q9. Backends? Azure App Configuration (.NET native), LaunchDarkly, GrowthBook, Unleash.
Q10. Server vs client side? Server: secure. Client: low latency for UI.
Q11. Metrics-driven rollout? Track success per branch; compare; decide ramp.
Q12. Environment gating with flags? Use labels (per-env) in App Configuration.
Gotchas / common mistakes
- ⚠️ Permanent flags instead of config.
- ⚠️ Flags for security.
- ⚠️ Flag combinatorics untestable.
- ⚠️ No cleanup — debt.
- ⚠️ No metrics rollout.