Tutorial December 13, 2025 6 min read

Advanced Feature Flag Patterns in .NET

You have mastered the basics. Now you need to change how existing code works, or add columns to tables with millions of rows. These patterns keep your codebase clean and your deployments safe.

This is Part 2. If you have not read it yet, start with Part 1: Implementing Features with Feature Flags which covers UI toggles and new features with database changes.

Scenario 3: Modifying Existing Behavior

The trickiest scenario. You need to change how something already works. Maybe you are changing the order calculation logic, or modifying how notifications are sent.

The key insight: do not modify existing code. Instead, create a new implementation and switch between them.

Pattern: Strategy with Flag-Based Selection

// Define the interface
public interface IOrderTotalCalculator
{
    decimal Calculate(Order order);
}

// Original implementation
public class LegacyOrderTotalCalculator : IOrderTotalCalculator
{
    public decimal Calculate(Order order)
    {
        // Original logic
        return order.Items.Sum(i => i.Price * i.Quantity);
    }
}

// New implementation with different logic
public class OrderTotalCalculatorV2 : IOrderTotalCalculator
{
    public decimal Calculate(Order order)
    {
        // New logic - handles bundles, volume discounts, etc.
        var subtotal = order.Items.Sum(i => i.Price * i.Quantity);
        var volumeDiscount = CalculateVolumeDiscount(order);
        return subtotal - volumeDiscount;
    }

    private decimal CalculateVolumeDiscount(Order order)
    {
        var totalItems = order.Items.Sum(i => i.Quantity);
        if (totalItems >= 10) return order.Items.Sum(i => i.Price * i.Quantity) * 0.05m;
        return 0;
    }
}
// Factory that selects implementation based on flag
public class OrderTotalCalculatorFactory
{
    private readonly IFlagitClient _flagit;
    private readonly LegacyOrderTotalCalculator _legacy;
    private readonly OrderTotalCalculatorV2 _v2;

    public OrderTotalCalculatorFactory(
        IFlagitClient flagit,
        LegacyOrderTotalCalculator legacy,
        OrderTotalCalculatorV2 v2)
    {
        _flagit = flagit;
        _legacy = legacy;
        _v2 = v2;
    }

    public async Task<IOrderTotalCalculator> GetCalculatorAsync(string userId)
    {
        if (await _flagit.IsEnabledAsync("order-calculator-v2", new { userId }))
        {
            return _v2;
        }
        return _legacy;
    }
}
// Usage in your service
public class OrderService
{
    private readonly OrderTotalCalculatorFactory _calculatorFactory;

    public async Task<OrderSummary> GetOrderSummaryAsync(int orderId, string userId)
    {
        var order = await _db.Orders
            .Include(o => o.Items)
            .FirstAsync(o => o.Id == orderId);

        var calculator = await _calculatorFactory.GetCalculatorAsync(userId);
        var total = calculator.Calculate(order);

        return new OrderSummary
        {
            OrderId = orderId,
            Total = total,
            // ...
        };
    }
}

Why this pattern works

  • Both implementations exist side by side - easy to compare behavior
  • Flag check happens once, at the factory level
  • When removing the flag: delete factory, legacy class, inject V2 directly
  • Easy to test both implementations independently

Scenario 4: Adding a Column to Existing Table

You need to add a new column to an existing table and use it in your feature. This requires careful sequencing to avoid downtime.

The Sequence

1

Add nullable column (or with default)

Deploy migration that adds the column as nullable. Existing code continues to work.

2

Deploy code that writes to the column

Behind feature flag, start populating the new column. Old rows have null.

3

Backfill existing data (if needed)

Run a migration or background job to populate the column for existing rows.

4

Enable flag, deploy code that reads the column

Now the column has data, enable the feature for users.

5

Remove flag, make column non-nullable (optional)

Once fully rolled out, clean up the flag and optionally add NOT NULL constraint.

// Step 1: Migration - add nullable column
public partial class AddOrderPriorityColumn : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<int>(
            name: "Priority",
            table: "Orders",
            nullable: true); // Nullable so existing rows are fine
    }
}

// Step 2: Code that writes (behind flag)
public async Task<Order> CreateOrderAsync(CreateOrderRequest request, string userId)
{
    var order = new Order
    {
        CustomerId = request.CustomerId,
        Items = request.Items,
        CreatedAt = DateTime.UtcNow
    };

    // Only set priority if feature is enabled
    if (await _flagit.IsEnabledAsync("order-priority", new { userId }))
    {
        order.Priority = CalculatePriority(request);
    }

    _db.Orders.Add(order);
    await _db.SaveChangesAsync();
    return order;
}

// Step 3: Code that reads (also behind flag)
public async Task<List<Order>> GetOrdersAsync(string userId)
{
    var query = _db.Orders.Where(o => o.CustomerId == userId);

    if (await _flagit.IsEnabledAsync("order-priority", new { userId }))
    {
        // New behavior: sort by priority
        query = query.OrderByDescending(o => o.Priority ?? 0)
                     .ThenByDescending(o => o.CreatedAt);
    }
    else
    {
        // Old behavior: sort by date only
        query = query.OrderByDescending(o => o.CreatedAt);
    }

    return await query.ToListAsync();
}

More Mistakes to Avoid

Nested flag checks

Bad: if (flagA && flagB && !flagC)

Good: One flag per feature. If you need complex logic, create a single flag that encapsulates it.

Not planning for cleanup

Bad: Creating a flag with no owner or expiration date.

Good: Set a cleanup date when creating the flag. Add a TODO comment in code linking to the flag.

Modifying existing code instead of replacing

Bad: Adding if/else branches inside existing methods.

Good: Create new implementation class, use factory to switch between them.

Cleanup Checklist

When your feature is fully rolled out (100% for at least a week), it is time to remove the flag:

  • Delete the flag from Flagit dashboard
  • Remove all IsEnabledAsync calls for this flag
  • Delete the "old" code path (the else branch)
  • Delete any legacy service implementations
  • Remove factory classes if they only existed for the flag
  • Update tests to remove flag-related branches
  • Run tests to ensure everything still works

Pro tip: Set a calendar reminder when you create the flag. Flagit tracks flag age and can help you identify stale flags that should have been cleaned up.

Summary

  • Modifying behavior: Strategy pattern with flag-based factory
  • New columns: Nullable first, write behind flag, backfill, then read
  • Cleanup: Set reminders, delete old code paths, remove factories
  • Always: One flag per feature, plan for removal from day one