Feature Flags at Scale

Feature Flags at Scale

Julian Krause · March 08, 2026
Engineering CultureDevOpsArchitecture
ArchitectureEngineering Culture

Feature flags start simple: a boolean that gates a new feature. At scale — hundreds of flags across dozens of services — they become a governance challenge. Stale flags accumulate, flag interactions create unexpected behaviors, and the codebase becomes littered with conditional branches. Here is how to use feature flags effectively without creating a maintenance nightmare.

Microsoft.FeatureManagement in .NET

The Microsoft.FeatureManagement library provides a clean abstraction for feature flags in .NET, with built-in support for percentage rollouts, time windows, and custom filters.

builder.Services.AddFeatureManagement()
    .AddFeatureFilter<PercentageFilter>()
    .AddFeatureFilter<TimeWindowFilter>()
    .AddFeatureFilter<TargetingFilter>();

// Configuration in appsettings.json
// {
//   "FeatureManagement": {
//     "NewCheckoutFlow": {
//       "EnabledFor": [
//         {
//           "Name": "Percentage",
//           "Parameters": { "Value": 25 }
//         }
//       ]
//     }
//   }
// }

Use feature flags in controllers and services via dependency injection:

public class CheckoutController(IFeatureManager featureManager) : ControllerBase
{
    [HttpPost("checkout")]
    public async Task<IActionResult> Checkout(CheckoutRequest request)
    {
        if (await featureManager.IsEnabledAsync("NewCheckoutFlow"))
        {
            return await ProcessNewCheckoutAsync(request);
        }

        return await ProcessLegacyCheckoutAsync(request);
    }
}

Feature Flag Lifecycle Management

Every feature flag should have metadata: an owner, a creation date, an expected removal date, and a type classification. Without this, flags become permanent fixtures in your codebase.

// Define flags as strongly-typed constants with metadata
public static class FeatureFlags
{
    /// <summary>
    /// New checkout flow with Stripe Payment Elements.
    /// Owner: @julian | Created: 2026-02-01 | Remove by: 2026-04-01
    /// Type: Release toggle
    /// </summary>
    public const string NewCheckoutFlow = "NewCheckoutFlow";

    /// <summary>
    /// Extended analytics dashboard for Pro users.
    /// Owner: @team-analytics | Created: 2026-01-15 | Remove by: N/A
    /// Type: Permission toggle (permanent)
    /// </summary>
    public const string ExtendedAnalytics = "ExtendedAnalytics";
}

We run a weekly automated check that reports flags past their removal date. If a flag has been at 100% for more than two weeks and is past its removal date, it gets added to the tech debt backlog with high priority.

Flag Types Determine Lifecycle

Not all flags are the same. Categorizing them by type prevents the "remove all stale flags" discussion from conflating fundamentally different use cases.

Release toggles gate incomplete features during development. They are short-lived and should be removed within days of full rollout. Experiment toggles support A/B testing. They live for the duration of the experiment and are removed when results are collected. Ops toggles provide circuit breakers for operational control. They may be permanent but should be treated as infrastructure, not code. Permission toggles gate features based on subscription tiers or user roles. They are often permanent and should be modeled as proper authorization rules, not ad-hoc flags.

Avoid Flag Interactions

The most insidious problem at scale is flag interactions. If flag A and flag B are both active, the combined behavior might be untested and broken. With 100 flags, the number of possible combinations is astronomical.

// Dangerous — nested flags with interaction effects
if (await featureManager.IsEnabledAsync("NewPricing"))
{
    if (await featureManager.IsEnabledAsync("BulkDiscounts"))
    {
        // Has this combination been tested? Probably not.
        return CalculateNewPricingWithBulkDiscounts(order);
    }
    return CalculateNewPricing(order);
}

The mitigation is to limit the number of active flags (we cap at 20 per service), ensure each flag is independent, and use integration tests that cover flag combinations for critical paths.

Clean Up Relentlessly

A feature flag that has been at 100% rollout for a month is not a feature flag — it is dead code with extra steps. The cleanup cost grows over time as developers work around the flag in new code. Remove flags aggressively and automate the detection of stale ones. Your future self will thank you.


Comments (3)

David Feb 26, 2026 · 17:08

Could you elaborate on this topic in a follow-up post?

Greta Feb 21, 2026 · 20:08

I had the same experience, can confirm.

Hannah Mar 07, 2026 · 23:08

Exactly! I had the same thought.

Elena Feb 27, 2026 · 17:08

This is exactly what I was looking for, thank you!

Felix Feb 28, 2026 · 17:08

I have a question: does this also apply to older versions?


Leave a comment

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.