Building a Runtime Feature Flag Engine in .NET MAUI

🚩 Building a Runtime Feature Flag Engine in .NET MAUI

Modern mobile applications rarely behave exactly the same for every user. A feature might be available only to beta testers. A redesigned checkout experience might initially be enabled for 5% of customers. An experimental screen might need to disappear immediately if production telemetry reveals a problem. Enterprise customers may receive capabilities that aren't available in the standard edition. The traditional solution is usually some variation of:

    if (user.IsPremium)
    {
        // Show premium feature
    }

That works. Until it doesn't. As the application grows, these conditions begin appearing everywhere:

    if (configuration.EnableNewDashboard)
    {
    }
    
    if (user.Role == "Administrator")
    {
    }
    
    if (Preferences.Get("EnableExperimentalCheckout", false))
    {
    }
    
    if (DeviceInfo.Platform == DevicePlatform.Android)
    {
    }
    
    if (remoteConfig["EnableNewSearch"] == "true")
    {
    }

Soon, feature availability is scattered throughout pages, ViewModels, services, navigation code, configuration files, and platform-specific implementations.

More importantly, changing those decisions may require publishing another application version. For mobile applications, that's a significant limitation.

Users don't necessarily update immediately, store reviews take time, staged deployments can last days, and a production incident may require disabling functionality right now.

That's where a runtime feature flag engine becomes extremely useful. πŸš€

In this guide, we'll build a production-oriented feature flag architecture for .NET MAUI that supports:

  • 🚩 Runtime feature evaluation
  • πŸ’Ύ Persistent local caching
  • ☁️ Remote configuration
  • πŸ“± Offline operation
  • πŸ‘€ User targeting
  • 🏒 Tenant targeting
  • πŸ“± Platform targeting
  • πŸ“Š Percentage rollouts
  • πŸ§ͺ A/B experiments
  • πŸ›‘οΈ Kill switches
  • ⏳ Time-based activation
  • πŸ”„ Background refresh
  • 🧭 Navigation integration
  • πŸ–ΌοΈ UI integration
  • βš™οΈ Dependency Injection
  • πŸ§ͺ Automated testing
  • πŸ“ˆ Diagnostics and observability
  • πŸ” Security boundaries
  • πŸ—οΈ Enterprise architecture considerations

The goal isn't simply to add a few Boolean configuration values. We're going to build a small feature decision engine.


πŸ“š Table of Contents

  1. 🚩 What Is a Feature Flag?
  2. πŸ€” Why Feature Flags Matter in Mobile Applications
  3. ❌ The Naive Approach
  4. πŸ—οΈ Target Architecture
  5. 🧩 Feature Flag Types
  6. πŸ“¦ Designing the Core Contracts
  7. πŸ—‚οΈ Defining Feature Keys
  8. 🧠 Building the Evaluation Context
  9. πŸ“‹ Modeling Feature Definitions
  10. βš™οΈ Building the Feature Flag Engine
  11. πŸ”— Implementing Evaluation Rules
  12. πŸ“± Platform-Based Flags
  13. πŸ‘€ User-Based Targeting
  14. 🏒 Tenant-Based Targeting
  15. πŸ“Š Percentage Rollouts
  16. πŸ§ͺ A/B Testing
  17. ⏳ Time-Based Flags
  18. πŸ›‘οΈ Kill Switches
  19. ☁️ Remote Feature Configuration
  20. πŸ’Ύ Building an Offline Cache
  21. πŸ”„ Runtime Refresh
  22. πŸ“‘ Connectivity-Aware Synchronization
  23. βš™οΈ Dependency Injection
  24. πŸ–ΌοΈ Feature Flags in the UI
  25. 🧭 Feature Flags and Navigation
  26. 🧱 Feature-Gated Services
  27. 🧩 Feature Flags and Modular Architectures
  28. πŸ” Security Considerations
  29. ⚑ Performance and Caching
  30. πŸ“ˆ Diagnostics and Observability
  31. πŸ§ͺ Testing the Engine
  32. 🏒 Multi-Tenant Applications
  33. πŸš€ Progressive Rollouts
  34. πŸ”™ Rollback Strategies
  35. ⚠️ Common Mistakes
  36. 🧹 Feature Flag Lifecycle Management
  37. πŸ—οΈ Suggested Project Structure
  38. πŸ“Š Architecture Comparison
  39. 🏒 Real-World Enterprise Scenario
  40. βœ… Best Practices
  41. 🎯 Final Thoughts
  42. πŸ“š References

🚩 1. What Is a Feature Flag?

A feature flag, also called a feature toggle, is a runtime decision that determines whether a capability should be enabled. The simplest possible implementation is:

    bool enableNewDashboard = true;
    
    if (enableNewDashboard)
    {
        // Use new dashboard.
    }

But production feature management is considerably more powerful. Instead of asking:

Is NewDashboard enabled?

we might ask:

Is NewDashboard enabled for this particular user, tenant, platform, application version, environment, and rollout bucket at this moment?

That distinction is important. A mature feature flag system is effectively a policy evaluation engine.


πŸ€” 2. Why Feature Flags Matter in Mobile Applications

Feature flags are useful everywhere, but mobile development introduces several constraints that make them especially valuable.

When deploying a web application, you can often replace the server deployment immediately. With mobile applications:

  1. You publish a new version.
  2. The store processes it.
  3. Users receive the update.
  4. Some users install it immediately.
  5. Others wait days or weeks.
  6. Some may never update.

This means several application versions can exist simultaneously in production.

Feature flags provide another layer of control.

                        Application Release
                               β”‚
                               β–Ό
                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                      β”‚ Installed Build β”‚
                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                               β–Ό
                     Runtime Feature Engine
                               β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β–Ό              β–Ό              β–Ό
             Enabled        Disabled       Variant
    

You can change behavior without necessarily shipping another binary.

πŸ“Š Typical scenarios

Scenario Feature Flag Benefit
πŸš€ Gradual release Enable a feature for a small percentage first
🐞 Production defect Disable functionality remotely
πŸ§ͺ Experiment Compare two experiences
πŸ‘€ Beta program Enable features for selected users
🏒 Enterprise edition Enable capabilities per tenant
πŸ“± Platform issue Disable only on Android or iOS
🌎 Regional rollout Enable functionality gradually by market
πŸ”„ Backend migration Switch clients between implementations
πŸ›‘οΈ Emergency response Activate a kill switch
πŸ“¦ Legacy migration Keep old and new implementations available temporarily

❌ 3. The Naive Approach

A common implementation begins with configuration:

    public class FeatureOptions
    {
        public bool NewDashboard { get; set; }
    
        public bool NewCheckout { get; set; }
    
        public bool ExperimentalSearch { get; set; }
    }

Then:

    if (_options.NewDashboard)
    {
        await ShowNewDashboardAsync();
    } 

For a small application, this may be enough. The problems appear when requirements become dynamic. What if NewDashboard should be:

  • enabled for Android but not iOS,
  • enabled for employees,
  • enabled for 10% of customers,
  • disabled for app versions below 5.2,
  • enabled only after a specific date,
  • immediately disabled during an incident?

A Boolean isn't enough anymore. You need rules.


πŸ—οΈ 4. Target Architecture

Let's design the system around several responsibilities.

                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                               β”‚ Remote Configuration β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                           β”‚
                                           β–Ό
                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                  β”‚ Refresh Service β”‚
                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                           β”‚
                                           β–Ό
                                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                  β”‚ Persistent Cacheβ”‚
                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                           β”‚
                                           β–Ό
                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                               β”‚ Feature Flag Engine β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                          β”‚
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚                        β”‚                        β”‚
                 β–Ό                        β–Ό                        β–Ό
          Evaluation Context          Flag Definition         Rule Pipeline
                 β”‚                                                 β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β–Ό       β–Ό        β–Ό                         β–Ό              β–Ό             β–Ό
       User    Tenant   Platform                  User          Percentage      Time
                                                                              Rules
                                          β”‚
                                          β–Ό
                                  Feature Decision
                                          β”‚
                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                      β–Ό                   β–Ό                    β–Ό
                     UI              Navigation             Services
    

This architecture separates:

  • Where flags come from
  • How they are stored
  • How they're evaluated
  • Who is requesting the decision
  • What the application does with the result

That separation will become extremely important later.


🧩 5. Feature Flag Types

Not every feature flag serves the same purpose.

A useful classification looks like this:

Type Purpose Example
πŸš€ Release flag Progressive feature deployment New dashboard
πŸ§ͺ Experiment flag A/B testing Checkout A vs B
πŸ›‘οΈ Operational flag Runtime protection Disable image processing
🏒 Entitlement flag Customer capability Enterprise reports
πŸ“± Platform flag Platform-specific control New Android camera
πŸ”„ Migration flag Switch implementations REST v1 β†’ REST v2
πŸ§‘β€πŸ’» Development flag Internal/testing capability Debug diagnostics

These flags also have different expected lifetimes.

An experiment might exist for two weeks.

An entitlement may exist for years.

A kill switch may remain permanently available.

This matters because feature flags can easily become technical debt.

We'll return to that later.


πŸ“¦ 6. Designing the Core Contracts

Let's start with the engine itself.

    public interface IFeatureFlagEngine
    {
        Task<bool> IsEnabledAsync(
            string feature,
            FeatureContext context,
            CancellationToken cancellationToken = default);
    }
    

For applications where the engine can resolve the current user context automatically, we can provide a convenience abstraction:

    public interface IFeatureManager
    {
        Task<bool> IsEnabledAsync(
            string feature,
            CancellationToken cancellationToken = default);
    }

The distinction is useful.

IFeatureFlagEngine is the low-level evaluator.

IFeatureManager is the application-facing facade.


πŸ—‚οΈ 7. Defining Feature Keys

Avoid magic strings throughout the application. Bad:

    if (await _featureManager.IsEnabledAsync("new-dashbaord"))
    {
    }

Notice the typo. Instead:

    public static class Features
    {
        public const string NewDashboard = "new-dashboard";
        public const string NewCheckout = "new-checkout";
        public const string NewSearch = "new-search";
        public const string DiagnosticPanel = "diagnostic-panel";
        public const string EmergencyPaymentsKillSwitch =
            "emergency-payments-kill-switch";
    }

Now:

    if (await _featureManager.IsEnabledAsync(Features.NewDashboard))
    {
    }

For larger applications, you can go further and introduce strongly typed identifiers:

    public readonly record struct FeatureKey(string Value);

Then:

    public static class AppFeatures
    {
        public static readonly FeatureKey NewDashboard =
            new("new-dashboard");
    
        public static readonly FeatureKey NewCheckout =
            new("new-checkout");
    }

This makes accidental interchange with arbitrary strings harder.


🧠 8. Building the Evaluation Context

Feature decisions need context.

    public sealed record FeatureContext
    {
        public string? UserId { get; init; }
    
        public string? TenantId { get; init; }
    
        public string? Country { get; init; }
    
        public string? AppVersion { get; init; }
    
        public string? Environment { get; init; }
    
        public string Platform { get; init; } = string.Empty;
    
        public IReadOnlyCollection<string> Roles { get; init; }
            = Array.Empty<string>();
    }

In .NET MAUI we can populate some values automatically:

    public sealed class MauiFeatureContextProvider
    {
        public FeatureContext Create(
            string? userId,
            string? tenantId,
            IEnumerable<string>? roles = null)
        {
            return new FeatureContext
            {
                UserId = userId,
                TenantId = tenantId,
                Platform = DeviceInfo.Platform.ToString(),
                AppVersion = AppInfo.Current.VersionString,
                Roles = roles?.ToArray() ?? Array.Empty<string>()
            };
        }
    }

Now the engine knows not only which feature is being evaluated, but for whom and where.


πŸ“‹ 9. Modeling Feature Definitions

A feature definition needs more than a Boolean.

    public sealed class FeatureDefinition
    {
        public required string Key { get; init; }
    
        public bool Enabled { get; init; }
    
        public bool KillSwitch { get; init; }
    
        public int? RolloutPercentage { get; init; }
    
        public DateTimeOffset? StartsAtUtc { get; init; }
    
        public DateTimeOffset? EndsAtUtc { get; init; }
    
        public IReadOnlyCollection<string> Users { get; init; }
            = Array.Empty<string>();
    
        public IReadOnlyCollection<string> Tenants { get; init; }
            = Array.Empty<string>();
    
        public IReadOnlyCollection<string> Platforms { get; init; }
            = Array.Empty<string>();
    }

A remote representation might look like:

    {
      "key": "new-dashboard",
      "enabled": true,
      "killSwitch": false,
      "rolloutPercentage": 25,
      "platforms": [
        "Android",
        "iOS"
      ],
      "users": [],
      "tenants": [
        "enterprise-001"
      ]
    }

This already gives us significantly more control than a Boolean configuration value.


βš™οΈ 10. Building the Feature Flag Engine

Let's introduce a definition provider.

    public interface IFeatureDefinitionProvider
    {
        Task<FeatureDefinition?> GetAsync(
            string feature,
            CancellationToken cancellationToken = default);
    }

Now the engine:

    public sealed class FeatureFlagEngine : IFeatureFlagEngine
    {
        private readonly IFeatureDefinitionProvider _definitions;
    
        public FeatureFlagEngine(
            IFeatureDefinitionProvider definitions)
        {
            _definitions = definitions;
        }
    
        public async Task<bool> IsEnabledAsync(
            string feature,
            FeatureContext context,
            CancellationToken cancellationToken = default)
        {
            var definition = await _definitions.GetAsync(
                feature,
                cancellationToken);
    
            if (definition is null)
                return false;
    
            if (!definition.Enabled)
                return false;
    
            if (definition.KillSwitch)
                return false;
    
            if (!MatchesPlatform(definition, context))
                return false;
    
            if (!MatchesTenant(definition, context))
                return false;
    
            if (!MatchesUser(definition, context))
                return false;
    
            if (!MatchesTimeWindow(definition))
                return false;
    
            if (!MatchesRollout(definition, context))
                return false;
    
            return true;
        }
    }

This is our first real decision pipeline.

But putting every rule inside one class will eventually create another monolith.

We can improve it.


πŸ”— 11. Implementing Evaluation Rules

Create a rule abstraction.

    public interface IFeatureEvaluationRule
    {
        int Order { get; }
    
        ValueTask<bool> EvaluateAsync(
            FeatureDefinition definition,
            FeatureContext context,
            CancellationToken cancellationToken);
    }

Now individual policies become isolated.

    public sealed class PlatformRule : IFeatureEvaluationRule
    {
        public int Order => 100;
    
        public ValueTask<bool> EvaluateAsync(
            FeatureDefinition definition,
            FeatureContext context,
            CancellationToken cancellationToken)
        {
            if (definition.Platforms.Count == 0)
                return ValueTask.FromResult(true);
    
            var matches = definition.Platforms.Any(
                x => string.Equals(
                    x,
                    context.Platform,
                    StringComparison.OrdinalIgnoreCase));
    
            return ValueTask.FromResult(matches);
        }
    }

Then:

    public sealed class RuleBasedFeatureFlagEngine : IFeatureFlagEngine
    {
        private readonly IFeatureDefinitionProvider _definitions;
        private readonly IReadOnlyList<IFeatureEvaluationRule> _rules;
    
        public RuleBasedFeatureFlagEngine(
            IFeatureDefinitionProvider definitions,
            IEnumerable<IFeatureEvaluationRule> rules)
        {
            _definitions = definitions;
    
            _rules = rules
                .OrderBy(x => x.Order)
                .ToArray();
        }
    
        public async Task<bool> IsEnabledAsync(
            string feature,
            FeatureContext context,
            CancellationToken cancellationToken = default)
        {
            var definition = await _definitions.GetAsync(
                feature,
                cancellationToken);
    
            if (definition is null || !definition.Enabled)
                return false;
    
            foreach (var rule in _rules)
            {
                cancellationToken.ThrowIfCancellationRequested();
    
                if (!await rule.EvaluateAsync(
                        definition,
                        context,
                        cancellationToken))
                {
                    return false;
                }
            }
    
            return true;
        }
    }

Now the engine follows the Open/Closed Principle. Adding another rule doesn't require modifying the evaluator.


πŸ“± 12. Platform-Based Flags

Suppose a new camera implementation works well on Android but still has issues on iOS.

Instead of delaying the entire release:

    {
      "key": "new-camera",
      "enabled": true,
      "platforms": [
        "Android"
      ]
    }

The engine automatically rejects iOS:

    New Camera
         β”‚
         β–Ό
    Enabled?
      Yes
         β”‚
         β–Ό
    Platform Allowed?
         β”‚
     β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β”
     β”‚        β”‚
    Android   iOS
     β”‚        β”‚
     β–Ό        β–Ό
    YES       NO
    

This is extremely useful during platform-specific migrations.


πŸ‘€ 13. User-Based Targeting

Beta users can receive functionality first.

    public sealed class UserTargetingRule : IFeatureEvaluationRule
    {
        public int Order => 200;
    
        public ValueTask<bool> EvaluateAsync(
            FeatureDefinition definition,
            FeatureContext context,
            CancellationToken cancellationToken)
        {
            if (definition.Users.Count == 0)
                return ValueTask.FromResult(true);
    
            if (string.IsNullOrWhiteSpace(context.UserId))
                return ValueTask.FromResult(false);
    
            var enabled = definition.Users.Contains(
                context.UserId,
                StringComparer.OrdinalIgnoreCase);
    
            return ValueTask.FromResult(enabled);
        }
    }

Now your backend configuration could specify:

    {
      "key": "experimental-search",
      "enabled": true,
      "users": [
        "user-1002",
        "user-1048",
        "user-2025"
      ]
    }

🏒 14. Tenant-Based Targeting

For enterprise applications, tenant targeting is even more useful. Imagine three customers:

    Application
       β”‚
       β”œβ”€β”€ Contoso
       β”œβ”€β”€ Fabrikam
       └── AdventureWorks

Only Contoso purchased Advanced Analytics.

    {
      "key": "advanced-analytics",
      "enabled": true,
      "tenants": [
        "contoso"
      ]
    }

Your UI doesn't need hardcoded customer checks. Bad:

    if (_tenant.Name == "Contoso")
    {
    }

Better:

    if (await _features.IsEnabledAsync(
            Features.AdvancedAnalytics))
    {
    }

Business policy is moved out of presentation logic.


πŸ“Š 15. Percentage Rollouts

This is where feature flags become especially powerful. Suppose you want to release NewCheckout gradually.

Day 1

5%

Day 3

20%

Day 7

50%

Day 14

100%

A critical requirement is stable assignment. Don't do this:

    return Random.Shared.Next(0, 100) < percentage;

That would cause users to move in and out of the feature every time it is evaluated. 😬 Instead, calculate a deterministic bucket.

    public static int GetStableBucket(
        string feature,
        string userId)
    {
        var value = $"{feature}:{userId}";
    
        var bytes = SHA256.HashData(
            Encoding.UTF8.GetBytes(value));
    
        var number = BitConverter.ToUInt32(bytes, 0);
    
        return (int)(number % 100);
    }

Then:

    var bucket = GetStableBucket(
        definition.Key,
        context.UserId!);
    
    return bucket < definition.RolloutPercentage;

A user assigned to bucket 12 remains in bucket 12.


πŸ“Š Rollout Example

User Bucket 10% Rollout 25% Rollout 50% Rollout
πŸ‘€ A 4 βœ… βœ… βœ…
πŸ‘€ B 17 ❌ βœ… βœ…
πŸ‘€ C 38 ❌ ❌ βœ…
πŸ‘€ D 71 ❌ ❌ ❌
πŸ‘€ E 95 ❌ ❌ ❌

As rollout increases, previously enabled users remain enabled.

That's exactly what we want.


πŸ§ͺ 16. A/B Testing

Feature flags can also select variants. Instead of:

    bool enabled;

you may return:

    public sealed record FeatureDecision(
        bool Enabled,
        string? Variant = null);

Possible variants:

    checkout-control
    checkout-a
    checkout-b

For example:

| Bucket | Variant | | --: | --- | | 0–49 | πŸ…°οΈ Checkout A | | 50–99 | πŸ…±οΈ Checkout B | Then:

    var decision =
        await _featureManager.EvaluateAsync(
            Features.CheckoutExperiment);
    
    switch (decision.Variant)
    {
        case "checkout-a":
            await ShowCheckoutAAsync();
            break;
    
        case "checkout-b":
            await ShowCheckoutBAsync();
            break;
    
        default:
            await ShowLegacyCheckoutAsync();
            break;
    }

⚠️ Important

A feature flag engine can assign variants, but proper experimentation also requires:

  • Exposure tracking
  • Conversion events
  • Statistical analysis
  • Experiment start/end governance
  • Stable assignment
  • Privacy considerations

Feature flags and experimentation overlap, but they aren't exactly the same system.


⏳ 17. Time-Based Flags

Some features should activate automatically.

    public sealed class TimeWindowRule : IFeatureEvaluationRule
    {
        private readonly TimeProvider _timeProvider;
    
        public TimeWindowRule(TimeProvider timeProvider)
        {
            _timeProvider = timeProvider;
        }
    
        public int Order => 300;
    
        public ValueTask<bool> EvaluateAsync(
            FeatureDefinition definition,
            FeatureContext context,
            CancellationToken cancellationToken)
        {
            var now = _timeProvider.GetUtcNow();
    
            if (definition.StartsAtUtc is { } start &&
                now < start)
            {
                return ValueTask.FromResult(false);
            }
    
            if (definition.EndsAtUtc is { } end &&
                now >= end)
            {
                return ValueTask.FromResult(false);
            }
    
            return ValueTask.FromResult(true);
        }
    }

Using TimeProvider instead of directly calling DateTime.UtcNow makes testing much easier.


πŸ›‘οΈ 18. Kill Switches

One of the highest-value applications of feature management is the kill switch.

Imagine a new image processing engine causes crashes on certain Android devices.

Without remote control:

    Bug discovered
          ↓
    Fix developed
          ↓
    New build
          ↓
    Store submission
          ↓
    Review
          ↓
    User update

With a kill switch:

    Bug discovered
          ↓
    Remote flag changed
          ↓
    Clients refresh
          ↓
    Feature disabled

πŸ”₯ Huge operational difference.

A kill switch should usually take precedence over ordinary targeting.

                     Feature Evaluation
                            β”‚
                            β–Ό
                      Kill Switch?
                      /          \
                   YES            NO
                    β”‚              β”‚
                    β–Ό              β–Ό
                DISABLED      Continue Rules

For safety-critical operational flags, fail-safe behavior should be explicitly designed.


☁️ 19. Remote Feature Configuration

So far, our definitions could come from anywhere. That's intentional.

    public interface IRemoteFeatureSource
    {
        Task<IReadOnlyCollection<FeatureDefinition>> GetAsync(
            CancellationToken cancellationToken = default);
    }

Possible implementations include:

  • ☁️ Azure App Configuration
  • 🌐 Your REST API
  • πŸ”₯ Firebase Remote Config
  • πŸ“¦ Custom configuration service
  • 🏒 Internal enterprise configuration platform

Your engine should not depend directly on any one vendor.

    Azure App Configuration ─┐
                             β”‚
    Custom REST API ─────────┼──► IRemoteFeatureSource
                             β”‚
    Other Provider β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                      β”‚
                                      β–Ό
                             Feature Flag Engine

That abstraction protects the application architecture from provider-specific concerns.


πŸ’Ύ 20. Building an Offline Cache

Mobile applications cannot assume connectivity. If the application requires network access every time this executes:

    await _featureManager.IsEnabledAsync(...);

the architecture is already problematic. Feature evaluation should usually happen locally. Remote systems should refresh definitions, not participate in every decision.

    Remote Server
          β”‚
          β”‚ periodic refresh
          β–Ό
    Local Persistent Cache
          β”‚
          β–Ό
    In-Memory Snapshot
          β”‚
          β–Ό
    Feature Evaluation

Define storage:

    public interface IFeatureFlagStore
    {
        Task<IReadOnlyCollection<FeatureDefinition>> LoadAsync(
            CancellationToken cancellationToken = default);
    
        Task SaveAsync(
            IReadOnlyCollection<FeatureDefinition> definitions,
            CancellationToken cancellationToken = default);
    }

A JSON-backed implementation could serialize the current snapshot into FileSystem.AppDataDirectory.

    var path = Path.Combine(
        FileSystem.AppDataDirectory,
        "feature-flags.json");

Cache strategy

State Behavior
🌐 Online + cache exists Use cache immediately, refresh remotely
🌐 Online + no cache Fetch remote definitions
πŸ“΄ Offline + cache exists Use cached definitions
πŸ“΄ Offline + no cache Use safe defaults
❌ Remote refresh fails Keep last known good snapshot
⚠️ Remote payload invalid Reject update and keep previous snapshot

This pattern is extremely important.

Never destroy a known-good configuration because a refresh failed.


πŸ”„ 21. Runtime Refresh

Flags become far more useful when they can change while the app is running.

    public interface IFeatureRefreshService
    {
        Task RefreshAsync(
            CancellationToken cancellationToken = default);
    }

Implementation:

    public sealed class FeatureRefreshService
        : IFeatureRefreshService
    {
        private readonly IRemoteFeatureSource _remote;
        private readonly IFeatureFlagStore _store;
        private readonly FeatureSnapshotProvider _snapshot;
    
        public FeatureRefreshService(
            IRemoteFeatureSource remote,
            IFeatureFlagStore store,
            FeatureSnapshotProvider snapshot)
        {
            _remote = remote;
            _store = store;
            _snapshot = snapshot;
        }
    
        public async Task RefreshAsync(
            CancellationToken cancellationToken = default)
        {
            var definitions =
                await _remote.GetAsync(cancellationToken);
    
            await _store.SaveAsync(
                definitions,
                cancellationToken);
    
            _snapshot.Replace(definitions);
        }
    }

The refresh pipeline becomes:

    Refresh Requested
          β”‚
          β–Ό
    Download Configuration
          β”‚
          β–Ό
    Validate Configuration
          β”‚
          β–Ό
    Persist Snapshot
          β”‚
          β–Ό
    Swap In-Memory Snapshot
          β”‚
          β–Ό
    Notify Interested UI

Notice that the active snapshot changes only after the new configuration has been validated.


πŸ“‘ 22. Connectivity-Aware Synchronization

.NET MAUI exposes connectivity information through Connectivity. You can use it as a hint:

    if (Connectivity.Current.NetworkAccess ==
        NetworkAccess.Internet)
    {
        await _refreshService.RefreshAsync();
    }

You can also react to connectivity changes:

    Connectivity.Current.ConnectivityChanged +=
        OnConnectivityChanged;

But connectivity APIs don't guarantee that your backend is reachable.

The device might technically have internet access while:

  • DNS fails,
  • your API is unavailable,
  • authentication expired,
  • a captive portal intercepts requests.

Therefore:

Connectivity should optimize refresh behavior, not determine correctness.

Remote refresh must still handle network failures normally.


βš™οΈ 23. Dependency Injection

Register the architecture in MauiProgram.cs.

    builder.Services.AddSingleton<FeatureSnapshotProvider>();
    
    builder.Services.AddSingleton<IFeatureFlagStore, JsonFeatureFlagStore>();
    
    builder.Services.AddSingleton<IRemoteFeatureSource, ApiFeatureSource>();
    
    builder.Services.AddSingleton<IFeatureDefinitionProvider, CachedFeatureDefinitionProvider>();
    
    builder.Services.AddSingleton<IFeatureEvaluationRule, KillSwitchRule>();
    builder.Services.AddSingleton<IFeatureEvaluationRule, PlatformRule>();
    builder.Services.AddSingleton<IFeatureEvaluationRule, UserTargetingRule>();
    builder.Services.AddSingleton<IFeatureEvaluationRule, TenantTargetingRule>();
    builder.Services.AddSingleton<IFeatureEvaluationRule, TimeWindowRule>();
    builder.Services.AddSingleton<IFeatureEvaluationRule, PercentageRolloutRule>();
    
    builder.Services.AddSingleton<IFeatureFlagEngine, RuleBasedFeatureFlagEngine>();
    
    builder.Services.AddSingleton<IFeatureManager, FeatureManager>();
    
    builder.Services.AddSingleton<IFeatureRefreshService, FeatureRefreshService>();
    
    builder.Services.AddSingleton(TimeProvider.System);

The application layer only needs:

    IFeatureManager

Most pages shouldn't know anything about remote configuration, caching, hashing, targeting rules, or synchronization.


πŸ–ΌοΈ 24. Feature Flags in the UI

A ViewModel can evaluate a feature.

    public partial class HomeViewModel : ObservableObject
    {
        private readonly IFeatureManager _features;
    
        [ObservableProperty]
        private bool isNewDashboardEnabled;
    
        public HomeViewModel(IFeatureManager features)
        {
            _features = features;
        }
    
        public async Task InitializeAsync()
        {
            IsNewDashboardEnabled =
                await _features.IsEnabledAsync(
                    Features.NewDashboard);
        }
    }

XAML:

    <Button
        Text="Open New Dashboard"
        IsVisible="{Binding IsNewDashboardEnabled}" />

This is much cleaner than embedding feature evaluation logic throughout XAML converters or code-behind.


🧭 25. Feature Flags and Navigation

Flags can protect navigation too.

Hiding a button is not enough.

A route might still be reached through:

  • Deep links
  • Programmatic navigation
  • Restored navigation state
  • Notifications
  • Custom URI handlers

Create a navigation guard:

    public sealed class FeatureNavigationGuard
    {
        private readonly IFeatureManager _features;
    
        public FeatureNavigationGuard(
            IFeatureManager features)
        {
            _features = features;
        }
    
        public async Task<bool> CanNavigateAsync(
            string requiredFeature)
        {
            return await _features.IsEnabledAsync(
                requiredFeature);
        }
    }

Usage:

    if (!await _guard.CanNavigateAsync(
            Features.AdvancedReports))
    {
        return;
    }
    
    await Shell.Current.GoToAsync(
        nameof(AdvancedReportsPage));

πŸ›‘οΈ Defense in depth

    Feature Flag
        β”‚
        β”œβ”€β”€ UI Visibility
        β”‚
        β”œβ”€β”€ Navigation Guard
        β”‚
        └── Service / API Authorization

The feature flag improves UX.

The backend still enforces authorization.


🧱 26. Feature-Gated Services

Sometimes the feature controls an implementation rather than a screen.

Suppose you're migrating from a legacy API.

    public interface ICheckoutService
    {
        Task CheckoutAsync();
    }

Two implementations:

    public sealed class LegacyCheckoutService
        : ICheckoutService
    {
        public Task CheckoutAsync()
        {
            // Existing implementation.
            return Task.CompletedTask;
        }
    }
    

    public sealed class NewCheckoutService
        : ICheckoutService
    {
        public Task CheckoutAsync()
        {
            // New implementation.
            return Task.CompletedTask;
        }
    }

Feature-aware facade:

    public sealed class FeatureAwareCheckoutService
        : ICheckoutService
    {
        private readonly IFeatureManager _features;
        private readonly LegacyCheckoutService _legacy;
        private readonly NewCheckoutService _modern;
    
        public FeatureAwareCheckoutService(
            IFeatureManager features,
            LegacyCheckoutService legacy,
            NewCheckoutService modern)
        {
            _features = features;
            _legacy = legacy;
            _modern = modern;
        }
    
        public async Task CheckoutAsync()
        {
            if (await _features.IsEnabledAsync(
                    Features.NewCheckout))
            {
                await _modern.CheckoutAsync();
                return;
            }
    
            await _legacy.CheckoutAsync();
        }
    }

The consumer remains unaware of the migration.

πŸ”₯ This is an excellent pattern for safely replacing production implementations.


🧩 27. Feature Flags and Modular Architectures

Feature flags pair extremely well with modular/plugin architectures. Imagine:

    Host Application
          β”‚
          β”œβ”€β”€ Orders Module
          β”œβ”€β”€ Reports Module
          β”œβ”€β”€ Analytics Module
          └── Support Module

Feature flags can determine which modules appear in the user experience.

    if (await features.IsEnabledAsync(
            Features.Analytics))
    {
        menu.Add(analyticsMenuItem);
    }

But there's an important architectural distinction:

A runtime feature flag does not necessarily mean the assembly itself should be dynamically loaded or unloaded.

In many MAUI applications, the code remains packaged with the application while runtime flags control whether it is reachable or active.

This avoids unnecessary complexity with:

  • AOT
  • Trimming
  • Assembly loading
  • Native platform packaging
  • App Store constraints

πŸ” 28. Security Considerations

This deserves special attention.

🚨 Feature flags are not authorization. Never assume this:

    if (!await features.IsEnabledAsync("admin-panel"))
    {
        return;
    }

protects sensitive backend operations. A modified client could bypass the check. The server must independently authorize the operation.

Correct model

                 MAUI Client
                     β”‚
              Feature Flag Check
                     β”‚
                     β–Ό
                Show Feature
                     β”‚
                     β–Ό
                 API Request
                     β”‚
                     β–Ό
            Server Authorization
                     β”‚
                β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
                β–Ό         β–Ό
             Allowed    Denied

πŸ”’ Sensitive flag data

Avoid putting secrets into flag definitions. Bad:

    {
      "apiKey": "super-secret-key"
    }

Feature configuration should be treated as client-readable configuration. Even if stored securely, assume a determined user can inspect the application.


⚑ 29. Performance and Caching

Feature evaluation can occur frequently. Consider a page with:

12 buttons
8 menu entries
5 cards
4 navigation actions

If every decision performs an HTTP request, performance will be terrible. The evaluation path should ideally be:

    Feature Request
          β”‚
          β–Ό
    In-Memory Snapshot
          β”‚
          β–Ό
    Local Rule Evaluation
          β”‚
          β–Ό
    Decision

No network. No disk. No JSON parsing.

πŸ“Š Comparison

Strategy Latency Offline Recommended
🌐 Remote request per evaluation High ❌ ❌
πŸ’Ύ Disk read per evaluation Medium βœ… ❌
🧠 In-memory snapshot Very low βœ… βœ…
🧠 Memory + periodic refresh Very low βœ… ⭐ Best

Use immutable or effectively immutable snapshots whenever possible.


πŸ“ˆ 30. Diagnostics and Observability

When feature flags influence production behavior, you eventually need to answer:

Why did this user receive this experience?

A simple Boolean doesn't tell you. Consider a richer decision:

    public sealed record FeatureDecision(
        string Feature,
        bool Enabled,
        string Reason,
        string? Variant = null,
        DateTimeOffset? EvaluatedAt = null);

Possible reasons:

  • GloballyDisabled
  • KillSwitchActive
  • PlatformMismatch
  • TenantMismatch
  • UserTargeted
  • RolloutIncluded
  • RolloutExcluded
  • TimeWindowExpired
  • Enabled

Example:

    new FeatureDecision(
        Feature: "new-checkout",
        Enabled: false,
        Reason: "RolloutExcluded",
        EvaluatedAt: DateTimeOffset.UtcNow);

This is extremely useful for diagnostics.

πŸ“Š Useful telemetry

Track aggregate information such as:

  • Feature evaluated
  • Feature enabled
  • Variant selected
  • Configuration refreshed
  • Configuration refresh failed
  • Snapshot age
  • Invalid configuration rejected

Be careful with user identifiers and privacy-sensitive telemetry.


πŸ§ͺ 31. Testing the Engine

One major benefit of separating evaluation rules is testability.

Platform test

    [Fact]
    public async Task AndroidOnlyFeature_ShouldBeDisabledOnIos()
    {
        var definition = new FeatureDefinition
        {
            Key = "camera-v2",
            Enabled = true,
            Platforms = new[] { "Android" }
        };
    
        var context = new FeatureContext
        {
            Platform = "iOS"
        };
    
        var rule = new PlatformRule();
    
        var result = await rule.EvaluateAsync(
            definition,
            context,
            CancellationToken.None);
    
        Assert.False(result);
    }

Tenant test

    [Fact]
    public async Task Feature_ShouldBeEnabledForConfiguredTenant()
    {
        var definition = new FeatureDefinition
        {
            Key = "enterprise-reports",
            Enabled = true,
            Tenants = new[] { "contoso" }
        };
    
        var context = new FeatureContext
        {
            TenantId = "contoso"
        };
    
        var rule = new TenantTargetingRule();
    
        Assert.True(await rule.EvaluateAsync(
            definition,
            context,
            CancellationToken.None));
    }

Rollout stability test

    [Fact]
    public void Bucket_ShouldBeStable()
    {
        var first = StableBucket.Get(
            "new-dashboard",
            "user-123");
    
        var second = StableBucket.Get(
            "new-dashboard",
            "user-123");
    
        Assert.Equal(first, second);
    }

Offline fallback test

    [Fact]
    public async Task FailedRefresh_ShouldKeepPreviousSnapshot()
    {
        // Arrange known-good cached configuration.
    
        // Simulate remote failure.
    
        // Verify active configuration remains unchanged.
    }

These tests become essential once flags influence critical workflows.


🏒 32. Multi-Tenant Applications

Enterprise MAUI applications frequently support multiple organizations from one application binary. Feature flags can act as a runtime capability layer.

                        MAUI Application
                               β”‚
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β–Ό             β–Ό             β–Ό
              Tenant A      Tenant B      Tenant C
                 β”‚             β”‚             β”‚
              Standard      Premium       Custom

Configuration:

    {
      "features": {
        "advanced-reports": {
          "enabled": true,
          "tenants": [
            "tenant-b",
            "tenant-c"
          ]
        }
      }
    }

This is useful for:

  • 🏒 Enterprise licensing
  • 🎨 White-label experiences
  • 🧩 Optional modules
  • 🌎 Regional functionality
  • πŸ§ͺ Customer-specific pilots

However, commercial entitlements should normally also be enforced server-side.


πŸš€ 33. Progressive Rollouts

A safe rollout might look like:

    Internal Users
          β”‚
          β–Ό
         1%
          β”‚
          β–Ό
         5%
          β”‚
          β–Ό
        10%
          β”‚
          β–Ό
        25%
          β”‚
          β–Ό
        50%
          β”‚
          β–Ό
       100%

At every stage you monitor:

  • πŸ’₯ Crash rate
  • ⏱️ Response times
  • πŸ“‰ Failure rates
  • πŸ‘€ User feedback
  • πŸ“Š Conversion
  • πŸ”‹ Resource consumption

If something degrades:

    50%
     β”‚
     β–Ό
    Problem detected
     β”‚
     β–Ό
    Kill switch / rollback
     β”‚
     β–Ό
    0%

No new mobile binary required for the flag change itself.


πŸ”™ 34. Rollback Strategies

Feature flags enable several rollback patterns.

πŸ›‘οΈ Immediate Disable

New implementation β†’ OFF
Legacy implementation β†’ ON

πŸ“‰ Rollout Reduction

50% β†’ 10%

πŸ“± Platform Rollback

Android β†’ OFF
iOS β†’ ON

🏒 Tenant Rollback

Tenant A β†’ OFF
Other tenants β†’ ON

This level of precision is difficult to achieve with application releases alone.


⚠️ 35. Common Mistakes

❌ Calling the Server for Every Evaluation

Feature decisions should generally be local.


❌ Using Random Percentage Assignment

    Random.Shared.Next(...)

will create inconsistent experiences. Use deterministic bucketing.


❌ Treating Flags as Authorization

The client cannot be trusted as the final security boundary.


❌ Keeping Flags Forever

Old flags accumulate quickly.

  • NewDashboard
  • NewDashboardV2
  • UseNewDashboard
  • DashboardExperiment
  • DashboardMigration
  • DashboardFinal

😡 Remove obsolete flags.


❌ Feature Logic Everywhere

Avoid:

    if (flag)

in dozens of unrelated classes. Prefer feature-aware abstractions when behavior becomes complex.


❌ No Offline Strategy

Mobile connectivity is unreliable by definition. Always define fallback behavior.


❌ Replacing Good Configuration with Bad Data

Remote refresh should be transactional:

    Download
       ↓
    Validate
       ↓
    Persist
       ↓
    Activate

Never:

    Download
       ↓
    Immediately Activate
       ↓
    Discover Invalid Configuration πŸ’₯

🧹 36. Feature Flag Lifecycle Management

Feature flags should have owners. A useful metadata model might include:

    public sealed record FeatureMetadata(
        string Owner,
        string Purpose,
        DateTimeOffset CreatedAt,
        DateTimeOffset? ExpectedRemovalDate);

Example:

    Feature: new-checkout
    Owner: Payments Team
    Type: Release
    Created: 2026-04-01
    Expected Removal: 2026-06-15

Once rollout reaches 100% and the old implementation is removed, the flag should usually disappear too.

πŸ“Š Suggested policy

Flag Type Typical Lifetime
πŸš€ Release Days / weeks
πŸ§ͺ Experiment Weeks
πŸ”„ Migration Weeks / months
πŸ›‘οΈ Operational Long-lived
🏒 Entitlement Long-lived
πŸ§‘β€πŸ’» Development Short-lived

Without lifecycle management, feature flags become architecture debt.


πŸ—οΈ 37. Suggested Project Structure

A clean implementation could look like:

    Features/
    β”‚
    β”œβ”€β”€ Abstractions/
    β”‚   β”œβ”€β”€ IFeatureManager.cs
    β”‚   β”œβ”€β”€ IFeatureFlagEngine.cs
    β”‚   β”œβ”€β”€ IFeatureDefinitionProvider.cs
    β”‚   β”œβ”€β”€ IFeatureFlagStore.cs
    β”‚   └── IRemoteFeatureSource.cs
    β”‚
    β”œβ”€β”€ Models/
    β”‚   β”œβ”€β”€ FeatureDefinition.cs
    β”‚   β”œβ”€β”€ FeatureContext.cs
    β”‚   └── FeatureDecision.cs
    β”‚
    β”œβ”€β”€ Evaluation/
    β”‚   β”œβ”€β”€ FeatureFlagEngine.cs
    β”‚   β”œβ”€β”€ KillSwitchRule.cs
    β”‚   β”œβ”€β”€ PlatformRule.cs
    β”‚   β”œβ”€β”€ UserTargetingRule.cs
    β”‚   β”œβ”€β”€ TenantTargetingRule.cs
    β”‚   β”œβ”€β”€ TimeWindowRule.cs
    β”‚   └── PercentageRolloutRule.cs
    β”‚
    β”œβ”€β”€ Storage/
    β”‚   β”œβ”€β”€ JsonFeatureFlagStore.cs
    β”‚   └── FeatureSnapshotProvider.cs
    β”‚
    β”œβ”€β”€ Remote/
    β”‚   └── ApiFeatureSource.cs
    β”‚
    β”œβ”€β”€ Synchronization/
    β”‚   └── FeatureRefreshService.cs
    β”‚
    └── Diagnostics/
        └── FeatureDiagnostics.cs

This is significantly easier to evolve than a single FeatureService containing everything.


πŸ“Š 38. Architecture Comparison

Capability Hardcoded Boolean Remote Boolean Runtime Feature Engine
🚩 Enable/disable βœ… βœ… βœ…
☁️ Remote changes ❌ βœ… βœ…
πŸ“΄ Offline support βœ… ⚠️ βœ…
πŸ‘€ User targeting ❌ ⚠️ βœ…
🏒 Tenant targeting ❌ ⚠️ βœ…
πŸ“± Platform targeting ❌ ⚠️ βœ…
πŸ“Š Percentage rollout ❌ ❌ βœ…
πŸ§ͺ Experiments ❌ ❌ βœ…
πŸ›‘οΈ Kill switches ❌ βœ… βœ…
⏳ Scheduling ❌ ⚠️ βœ…
πŸ“ˆ Decision diagnostics ❌ ❌ βœ…
πŸ§ͺ Rule-level testing ❌ ❌ βœ…
πŸ”„ Last-known-good fallback ❌ ⚠️ βœ…

🏒 39. Real-World Enterprise Scenario

Imagine a retail application used by thousands of employees.

A new inventory scanner is ready.

The company doesn't want to release it to everyone immediately.

Stage 1 β€” Development

    Employees in development group only

Stage 2 β€” Pilot Stores

    Tenant / Store IDs:
    101
    204
    309

Stage 3 β€” Android Rollout

Platform: Android
Rollout: 10%

Stage 4 β€” Expanded Rollout

Platform: Android
Rollout: 50%

Stage 5 β€” All Android Users

Android: 100%
iOS: 0%

Stage 6 β€” Full Deployment

Android: 100%
iOS: 100%

Then a crash appears on a specific platform version. Instead of rolling back the entire application:

    Inventory Scanner
           β”‚
           β”œβ”€β”€ Android affected version β†’ OFF
           β”‚
           β”œβ”€β”€ Android other versions β†’ ON
           β”‚
           └── iOS β†’ ON

That is the operational power of feature flags.


βœ… 40. Best Practices

🚩 Flag Design

  • βœ… Give every feature a stable identifier.
  • βœ… Define safe defaults.
  • βœ… Classify flags by purpose.
  • βœ… Assign ownership.
  • βœ… Define expected removal dates for temporary flags.

🧠 Evaluation

  • βœ… Evaluate locally.
  • βœ… Use deterministic rollout buckets.
  • βœ… Make rules independently testable.
  • βœ… Return diagnostic reasons when useful.
  • βœ… Use immutable snapshots.

☁️ Remote Configuration

  • βœ… Treat remote configuration as synchronization.
  • βœ… Validate before activation.
  • βœ… Preserve last-known-good configuration.
  • βœ… Handle remote failures gracefully.
  • βœ… Never put secrets in feature definitions.

πŸ“΄ Offline

  • βœ… Persist a snapshot.
  • βœ… Define first-launch defaults.
  • βœ… Never require connectivity for normal evaluation.
  • βœ… Treat connectivity APIs as hints.

πŸ”’ Security

  • βœ… Never replace server authorization with feature flags.
  • βœ… Assume client-side configuration can be inspected.
  • βœ… Protect commercial entitlements server-side.
  • βœ… Minimize sensitive targeting data.

⚑ Performance

  • βœ… Keep decisions in memory.
  • βœ… Avoid HTTP calls during evaluation.
  • βœ… Avoid disk access during evaluation.
  • βœ… Cache deterministic context where appropriate.

πŸ§ͺ Testing

  • βœ… Test every rule independently.
  • βœ… Test percentage stability.
  • βœ… Test offline startup.
  • βœ… Test corrupted configuration.
  • βœ… Test failed refresh.
  • βœ… Test kill-switch precedence.
  • βœ… Test navigation protection.

🧹 Maintenance

  • βœ… Remove completed rollout flags.
  • βœ… Remove obsolete code paths.
  • βœ… Track flag ownership.
  • βœ… Audit long-lived flags regularly.

🎯 41. Final Thoughts

Feature flags are often introduced as a simple mechanism:

    if (enabled)
    {
    }

But at scale, they become much more than Boolean configuration.

A well-designed runtime feature flag engine can become an operational control plane for your .NET MAUI application.

It allows teams to: πŸš€ release features progressively, πŸ§ͺ run controlled experiments, πŸ›‘οΈ disable problematic functionality, πŸ“± isolate platform-specific issues, 🏒 customize enterprise experiences, πŸ‘€ target specific users, πŸ“Š control percentage rollouts, πŸ”„ migrate between implementations, and πŸ“΄ continue making decisions even when the device is offline.

The most important architectural principle is to separate feature evaluation from feature synchronization.

Your application shouldn't contact a remote service every time it needs a decision.

Instead:

    Remote Configuration
            β”‚
            β–Ό
         Refresh
            β”‚
            β–Ό
    Persistent Last-Known-Good Snapshot
            β”‚
            β–Ό
    In-Memory Configuration
            β”‚
            β–Ό
    Local Decision Engine
            β”‚
            β–Ό
    Application Behavior

That design is fast, resilient, testable, provider-independent, and appropriate for the realities of mobile applications.

And perhaps the biggest benefit is operational.

A traditional release answers:

What code did we ship?

A runtime feature engine lets you answer another question:

What functionality should this particular user receive right now?

For large .NET MAUI applications, that can be an extremely powerful capability. πŸš€


πŸ“š 42. References

Resource Description
πŸ“˜ .NET MAUI Documentation Official .NET MAUI documentation
🚩 Feature management in .NET Microsoft's .NET feature-management reference
☁️ Azure App Configuration Centralized configuration and feature management
🚩 Azure App Configuration Feature Management Feature flag concepts and management
πŸ“± .NET MAUI Connectivity Network connectivity APIs in MAUI
πŸ’Ύ .NET MAUI File System Helpers Application data and cache directories
πŸ” .NET MAUI Secure Storage Securely storing small key/value data
βš™οΈ .NET MAUI Dependency Injection DI architecture in .NET MAUI
🧭 .NET MAUI Shell Navigation Shell navigation and route architecture
πŸ“± .NET MAUI Device Information Platform/device context
πŸ“¦ .NET MAUI App Information Application version and package information
⏰ TimeProvider Testable abstraction for time-based rules
πŸ” SHA256 Deterministic hashing foundation for rollout buckets
πŸ§ͺ xUnit Unit testing framework for .NET
🧰 .NET Community Toolkit .NET helper libraries
πŸ“± .NET Community Toolkit MVVM MVVM helpers useful for feature-aware ViewModels

πŸš€ Where to Go From Here

Once this architecture is working, several interesting extensions become possible:

  • πŸ“Š Build an A/B Experiment Engine
  • 🧩 Integrate flags with a Plugin Architecture
  • πŸ“ˆ Build a Feature Telemetry Dashboard
  • πŸ“΄ Add a Self-Healing Offline Configuration Layer
  • πŸ”„ Implement server-driven UI capabilities
  • 🏒 Build tenant-specific capability policies
  • 🧬 Generate strongly typed feature keys with a Roslyn Source Generator
  • πŸ›‘οΈ Add automated kill-switch health policies
  • πŸš€ Create a complete progressive delivery platform for .NET MAUI

At that point, feature flags stop being a collection of if statements and become a genuine part of your application's runtime architecture. πŸš©πŸš€


Was this useful?

Comments (0)

Leave a comment

Submit for moderation
An unhandled error has occurred. Reload πŸ—™