Skip to content

Feature Flags

Key Points

  • Feature flags decouple deploy from release. Code ships; flag stays off; flip on at runtime when ready.
  • Microsoft.FeatureManagement is 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)

{ "id": "NewBilling", "enabled": true }

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)

var variant = await fm.GetVariantAsync("ButtonColor");
return variant.Name;   // "blue" / "green"

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:

public class C(IFeatureManagerSnapshot fm) { /* same value throughout request */ }

Patterns

Kill switches

if (await fm.IsEnabledAsync("ExpensiveFeatureKilled")) return Disabled();

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.

o.UseFeatureFlags(ff => ff.SetRefreshInterval(TimeSpan.FromSeconds(30)));

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"

if (Environment.GetEnvironmentVariable("FEATURE_X") == "true") ...

Requires redeploy to flip.

✅ Correct: managed flag

if (await fm.IsEnabledAsync("FeatureX")) ...

❌ Wrong: flag for auth

if (await fm.IsEnabledAsync("AdminAccess") && /* something */) AllowAdmin();

Auth must be enforced in policy/code, not toggled.

✅ Correct: flag for behavior, auth via [Authorize]

[Authorize(Policy = "Admin")]
public IActionResult AdminPanel() { /* ... */ }

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.

Further reading