
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:
- Friday 5 PM: Deploy code to production (flag disabled)
- Monday 9 AM: Enable for internal users only
- Tuesday: Roll out to 10% of production users
- Wednesday: Increase to 50% if metrics look good
- Thursday: Full rollout to 100%
- 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:
- Use Azure App Configuration for feature flags (tight Azure integration, reasonable cost)
- Keep appsettings.json for environment configuration and static settings
- Implement module-based architecture where possible instead of scattered conditionals
- Establish flag lifecycle process with regular cleanup
- Add monitoring and alerting on flag state changes
- 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.