Feature Flags vs Configuration Files: Choosing the Right Approach for Dynamic Application Control

Modern application development requires flexibility in how we control feature releases and application behavior. Two common approaches are traditional configuration files (web.config for .NET Framework or appsettings.json for .NET Core/.NET) and dynamic feature flags. While both can control application behavior, they serve fundamentally different purposes and offer distinct advantages depending on your needs.

Traditional Configuration Management

Web.config (.NET Framework)

For .NET Framework applications, web.config has long been the standard for application configuration:

<configuration>
  <appSettings>
    <add key="EnableNewFeature" value="false" />
    <add key="MaxRetryAttempts" value="3" />
  </appSettings>
</configuration>

appsettings.json (.NET Core/.NET)

Modern .NET applications use appsettings.json with environment-specific overrides:

{
  "Features": {
    "EnableNewAlgorithm": false,
    "UseAdvancedCaching": true
  },
  "ConnectionStrings": {
    "DefaultConnection": "Server=..."
  }
}

Advantages of Configuration Files

  • Simple and straightforward – No external dependencies required
  • Version controlled – Changes tracked alongside code
  • Offline capability – Works without external services
  • Well-understood – Familiar to all .NET developers
  • Type-safe access – Strong typing through configuration classes

Disadvantages

  • Requires deployment – Even for simple toggles
  • Application restart – Changes to web.config trigger app pool recycling
  • Slower rollback – Must redeploy previous version
  • All or nothing – Difficult to do gradual rollouts

The Multi-Instance Challenge

When running applications behind a load balancer or Application Gateway with multiple instances, configuration file management becomes more complex.

Load Balanced Environment Considerations

With multiple web servers or App Service instances:

Configuration file approach:

  • Must update configuration on every instance
  • Risk of inconsistent behavior if instances are out of sync
  • Each instance restarts independently
  • Rolling deployments create temporary mixed-version states

Azure App Service handles this reasonably well:

  • Automated deployment to all instances
  • Slot swapping for zero-downtime deployments
  • Configuration transformations for different environments

Virtual Machine Scale Sets require additional orchestration:

  • Deployment scripts to update all VMs
  • Configuration management tools (Ansible, Chef, Puppet)
  • Custom startup scripts to pull configuration

Pipeline-Managed Configuration

Modern CI/CD pipelines significantly improve configuration management:

# Example Azure DevOps pipeline
- task: FileTransform@1
  inputs:
    folderPath: '$(System.DefaultWorkingDirectory)'
    fileType: 'xml'
    targetFiles: '**/web.config'

- task: AzureRmWebAppDeployment@4
  inputs:
    azureSubscription: 'Production'
    appType: 'webApp'
    WebAppName: 'myapp'

Benefits:

  • Automated, consistent deployment across all instances
  • Configuration transformations per environment
  • Audit trail through source control
  • Rollback capabilities via pipeline re-run
  • Approval gates for production changes

Limitations remain:

  • Still requires running a deployment
  • Application restarts still occur
  • Changes take minutes, not seconds
  • Emergency rollbacks require full pipeline execution

Azure Feature Flags: A Dynamic Alternative

Azure App Configuration provides managed feature flags that decouple deployment from feature releases.

Setting Up Azure App Configuration

// Program.cs / Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddAzureAppConfiguration();
    
    services.AddFeatureManagement()
        .AddFeatureFilter<PercentageFilter>()
        .AddFeatureFilter<TargetingFilter>();
}

public void Configure(IApplicationBuilder app)
{
    app.UseAzureAppConfiguration();
}

Using Feature Flags in Code

public class PaymentService
{
    private readonly IFeatureManager _featureManager;
    
    public PaymentService(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }
    
    public async Task<PaymentResult> ProcessPayment(PaymentRequest request)
    {
        if (await _featureManager.IsEnabledAsync("NewPaymentGateway"))
        {
            return await ProcessWithNewGateway(request);
        }
        
        return await ProcessWithLegacyGateway(request);
    }
}

Advantages of Feature Flags

Runtime control:

  • No deployment required to change behavior
  • Changes take effect within seconds (based on cache refresh)
  • No application restarts needed

Advanced targeting:

{
  "id": "NewCheckoutFlow",
  "enabled": true,
  "conditions": {
    "client_filters": [
      {
        "name": "Microsoft.Percentage",
        "parameters": {
          "Value": 20
        }
      }
    ]
  }
}

Centralized management:

  • Single source of truth across all instances
  • All servers read from the same service
  • Consistent behavior regardless of instance count

Operational benefits:

  • Instant rollback via toggle
  • A/B testing and gradual rollouts
  • User-specific or percentage-based activation
  • Kill switches for problematic features

Decoupling Deployment from Release

One of the most powerful aspects of feature flags is separating code deployment from feature releases.

Traditional Approach

Code Complete → QA Testing → Deploy to Production → Feature Live

Everything happens together. If marketing isn’t ready or a bug is found, the entire deployment is blocked.

Feature Flag Approach

Deploy to Production (flag disabled) → Internal Testing →
Gradual Rollout → Full Release

Example scenario:

// New caching algorithm deployed but disabled
if (await _featureManager.IsEnabledAsync("AdvancedCaching"))
{
    return await _advancedCache.GetOrCreateAsync(key, factory);
}
else
{
    return await _standardCache.GetAsync(key) 
        ?? await factory();
}

Timeline:

  1. Friday 5 PM: Deploy code to production (flag disabled)
  2. Monday 9 AM: Enable for internal users only
  3. Tuesday: Roll out to 10% of production users
  4. Wednesday: Increase to 50% if metrics look good
  5. Thursday: Full rollout to 100%
  6. Any time: Instant disable if issues detected

This is particularly valuable for:

  • Breaking API changes that need coordination
  • Features requiring business/marketing alignment
  • High-risk changes needing gradual validation
  • Incomplete features that shouldn’t block other deployments

Alternative Feature Flag Solutions

While Azure App Configuration is excellent for Azure-hosted applications, several alternatives exist:

LaunchDarkly

The industry leader in feature flag management.

Advantages:

  • Most sophisticated targeting and segmentation
  • Excellent analytics and experimentation features
  • Real-time flag updates with minimal latency
  • Superior SDKs across all major platforms
  • Built-in A/B testing and metric tracking
  • Advanced workflow and approval processes

Considerations:

  • Pricing scales with usage (can be expensive)
  • External dependency outside your infrastructure
  • Best suited for organizations committed to feature flag-driven development

Example usage:

var client = new LdClient("sdk-key");
var user = User.Builder("user-key")
    .Name("User Name")
    .Custom("tier", "premium")
    .Build();

if (client.BoolVariation("new-feature", user, false))
{
    // New feature code
}

Split.io

Similar to LaunchDarkly with strong emphasis on feature experimentation.

Strengths:

  • Excellent A/B testing capabilities
  • Built-in analytics tracking feature impact
  • Good balance of features and cost
  • Strong focus on measuring business outcomes

Unleash (Open Source)

Self-hosted feature flag solution.

Advantages:

  • Free and open source
  • Full control over infrastructure
  • No per-seat or usage-based pricing
  • Active community and good documentation

Considerations:

  • Requires infrastructure management
  • You’re responsible for uptime and scaling
  • Less sophisticated than commercial offerings

Setup:

var settings = new UnleashSettings()
{
    AppName = "my-app",
    UnleashApi = new Uri("https://your-unleash-instance.com/api/"),
    CustomHttpHeaders = new Dictionary<string, string>()
    {
        {"Authorization", "your-api-token"}
    }
};

var unleash = new DefaultUnleash(settings);

if (unleash.IsEnabled("new-feature"))
{
    // Feature code
}

ConfigCat

A simpler, more affordable option.

Advantages:

  • Lower cost than LaunchDarkly/Split
  • Easy to get started
  • Good for smaller teams
  • Supports all major platforms

Best for:

  • Startups and small to medium businesses
  • Teams new to feature flags
  • Budget-conscious organizations

Architectural Patterns for Feature Flags

Strategy Pattern with Feature Flags

Instead of scattered if/else statements, use dependency injection and the strategy pattern: csharp

public interface IPaymentProcessor
{
    Task<PaymentResult> Process(PaymentRequest request);
}

public class StandardPaymentProcessor : IPaymentProcessor
{
    public async Task<PaymentResult> Process(PaymentRequest request)
    {
        // Legacy implementation
    }
}

public class AdvancedPaymentProcessor : IPaymentProcessor
{
    public async Task<PaymentResult> Process(PaymentRequest request)
    {
        // New implementation
    }
}

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IPaymentProcessor>(provider =>
    {
        var featureManager = provider.GetRequiredService<IFeatureManager>();
        
        if (featureManager.IsEnabledAsync("AdvancedPayments").Result)
        {
            return new AdvancedPaymentProcessor();
        }
        
        return new StandardPaymentProcessor();
    });
}

Benefits:

  • Clean separation of implementations
  • Easier to remove old code later
  • Can swap entire subsystems
  • Better testability

Module-Based Loading

For larger features, load entire modules conditionally: csharp

public void ConfigureServices(IServiceCollection services)
{
    if (_featureManager.IsEnabledAsync("NewReportingEngine").Result)
    {
        services.AddScoped<IReportEngine, AdvancedReportEngine>();
        services.AddScoped<IReportRepository, OptimizedReportRepository>();
        services.AddScoped<IReportCache, DistributedReportCache>();
    }
    else
    {
        services.AddScoped<IReportEngine, LegacyReportEngine>();
        services.AddScoped<IReportRepository, StandardReportRepository>();
        services.AddScoped<IReportCache, InMemoryReportCache>();
    }
}

Best Practices

Flag Lifecycle Management

Not all flags are created equal. Categorize them:

Temporary flags – Features being rolled out:

release_new_checkout_flow_2025
experiment_pricing_page_v2

Set expiration reminders and remove after full rollout.

Permanent flags – Operational controls:

ops_disable_email_notifications
ops_enable_verbose_logging
circuit_breaker_external_api

Keep indefinitely as operational levers.

Naming conventions matter:
ops_ – Operational toggle
release_ – Release flag (temporary)
exp_ – Experiment/A/B test
kill_ – Emergency kill switch

Testing with Feature Flags

Test both code paths:

[Theory]
[InlineData(true)]  // New feature enabled
[InlineData(false)] // Legacy feature
public async Task TestPaymentProcessing(bool newFeatureEnabled)
{
    // Arrange
    var featureManagerMock = new Mock<IFeatureManager>();
    featureManagerMock
        .Setup(x => x.IsEnabledAsync("NewPaymentGateway"))
        .ReturnsAsync(newFeatureEnabled);
    
    var service = new PaymentService(featureManagerMock.Object);
    
    // Act
    var result = await service.ProcessPayment(testRequest);
    
    // Assert
    Assert.True(result.Success);
}

Monitoring and Observability

Track feature flag usage: csharp

public async Task<T> EvaluateFeature<T>(
    string featureName, 
    Func<Task<T>> enabledFunc, 
    Func<Task<T>> disabledFunc)
{
    var stopwatch = Stopwatch.StartNew();
    var isEnabled = await _featureManager.IsEnabledAsync(featureName);
    
    _logger.LogInformation(
        "Feature {FeatureName} evaluated to {State} in {ElapsedMs}ms",
        featureName, isEnabled, stopwatch.ElapsedMilliseconds);
    
    _metrics.Increment($"feature.{featureName}.{(isEnabled ? "enabled" : "disabled")}");
    
    try
    {
        return isEnabled 
            ? await enabledFunc() 
            : await disabledFunc();
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, 
            "Error in feature {FeatureName} (state: {State})", 
            featureName, isEnabled);
        throw;
    }
}

Security Considerations

Audit logging:

public class AuditedFeatureManager : IFeatureManager
{
    private readonly IFeatureManager _inner;
    private readonly IAuditLogger _auditLogger;
    
    public async Task<bool> IsEnabledAsync(string feature)
    {
        var result = await _inner.IsEnabledAsync(feature);
        
        await _auditLogger.LogAsync(new AuditEvent
        {
            EventType = "FeatureFlagEvaluation",
            FeatureName = feature,
            Result = result,
            Timestamp = DateTime.UtcNow,
            UserId = _currentUser.Id
        });
        
        return result;
    }
}

Role-based access control:

  • Not everyone should modify production flags
  • Separate permissions for viewing vs modifying
  • Require approvals for critical features
  • Maintain change history

When to Use Each Approach

Use Configuration Files When:

  • Settings are truly static (connection strings, API keys)
  • Changes are infrequent (quarterly or less)
  • Configuration should be version-controlled with code
  • No external dependencies are acceptable
  • Application restarts are not a concern
  • You have a solid deployment pipeline in place

Use Feature Flags When:

  • You need runtime control without deployments
  • Gradual rollouts are important
  • A/B testing is required
  • Quick rollback is critical
  • Multiple teams deploy to the same codebase
  • You practice continuous deployment
  • Features need user/percentage targeting

Hybrid Approach (Recommended)

Most applications benefit from both:

Configuration files for:

  • Environment-specific settings
  • Connection strings and secrets
  • Static operational parameters
  • Infrastructure configuration

Feature flags for:

  • New feature rollouts
  • A/B experiments
  • Kill switches
  • User-targeted features
  • Operational toggles

Implementation Recommendation

For a typical .NET application on Azure:

  1. Use Azure App Configuration for feature flags (tight Azure integration, reasonable cost)
  2. Keep appsettings.json for environment configuration and static settings
  3. Implement module-based architecture where possible instead of scattered conditionals
  4. Establish flag lifecycle process with regular cleanup
  5. Add monitoring and alerting on flag state changes
  6. Document flag dependencies and emergency procedures

Conclusion

Configuration files and feature flags are complementary tools, not mutually exclusive options. Configuration files remain excellent for static, environment-specific settings that rarely change. Feature flags excel at dynamic runtime control, enabling modern deployment practices like continuous delivery, gradual rollouts, and instant rollback.

The key is understanding when each approach provides the most value. For applications requiring flexibility, rapid iteration, and sophisticated release strategies, feature flags—whether through Azure App Configuration, LaunchDarkly, or other solutions—provide capabilities that traditional configuration simply cannot match.

As applications grow in complexity and teams adopt continuous deployment practices, the ability to decouple deployment from release becomes increasingly valuable. Feature flags make this possible while providing the safety nets needed to deploy confidently and frequently.

Posted in XAF

Leave a Reply

Your email address will not be published.