Skip to content

Feature Flag Stores

Pragmatic.FeatureFlags uses a pluggable store architecture. The IFeatureFlagStore interface handles both storage and evaluation of feature flags. This is different from IConfigurationStore (simple key-value) — feature flags require context-based evaluation with targeting rules, percentage rollout, etc.

Defined in Pragmatic.Abstractions:

public interface IFeatureFlagStore
{
Task<bool> IsEnabledAsync(string flagName, CancellationToken ct = default);
Task<bool> IsEnabledAsync(string flagName, FeatureFlagContext context, CancellationToken ct = default);
Task<FeatureFlagDefinition?> GetDefinitionAsync(string flagName, CancellationToken ct = default);
Task<IReadOnlyList<FeatureFlagDefinition>> GetAllAsync(CancellationToken ct = default);
IAsyncEnumerable<FeatureFlagChange> WatchAsync(CancellationToken ct = default);
}
MethodDescription
IsEnabledAsync(flagName)Evaluate without context (uses defaults). Unknown flags return false.
IsEnabledAsync(flagName, context)Evaluate with targeting context (tenant, user, plan, etc.)
GetDefinitionAsync(flagName)Get the full definition including rules. Returns null for unknown flags.
GetAllAsync()List all defined flags, ordered by name.
WatchAsync()Stream of FeatureFlagChange records for real-time change notification.

The default store for development and testing. Uses ConcurrentDictionary<string, FeatureFlagDefinition> with case-insensitive keys.

// Default: in-memory store
services.AddPragmaticFeatureFlags();

Beyond IFeatureFlagStore, the in-memory store exposes:

MethodDescription
Define(FeatureFlagDefinition)Create or update a flag definition
Remove(string flagName)Remove a flag definition. Returns true if the flag existed.

The InMemoryFeatureFlagStore delegates to FeatureFlagEvaluator (internal), which processes rules in order and falls back to the global Enabled state if no rule matches.

When Define() changes a flag’s Enabled state (global on/off), a FeatureFlagChange is emitted to WatchAsync() consumers. Changes to rules without changing the global state do not emit a notification.

The notification uses Channel<FeatureFlagChange> internally, providing unbounded buffering and natural backpressure.

// Default: in-memory store (via TryAdd)
services.AddPragmaticFeatureFlags();
// Custom store type
services.AddPragmaticFeatureFlags<MyDatabaseFeatureFlagStore>();
// Manual registration (register before AddPragmaticFeatureFlags)
services.AddSingleton<IFeatureFlagStore, MyCustomStore>();
services.AddPragmaticFeatureFlags(); // TryAdd won't replace your store

The generic overload AddPragmaticFeatureFlags<TStore>() uses AddSingleton (not TryAdd), so it always replaces any previous registration.

To back feature flags with a database, remote service, or other backend:

public class DatabaseFeatureFlagStore(
IDbConnectionFactory db,
ILogger<DatabaseFeatureFlagStore> logger) : IFeatureFlagStore
{
public async Task<bool> IsEnabledAsync(string flagName, CancellationToken ct = default)
=> await IsEnabledAsync(flagName, FeatureFlagContext.Empty, ct);
public async Task<bool> IsEnabledAsync(
string flagName, FeatureFlagContext context, CancellationToken ct = default)
{
var definition = await GetDefinitionAsync(flagName, ct);
if (definition is null)
return false; // Unknown flag = disabled
// Use the built-in evaluator or implement custom evaluation logic
return FeatureFlagEvaluator.Evaluate(definition, context);
}
public async Task<FeatureFlagDefinition?> GetDefinitionAsync(
string flagName, CancellationToken ct = default)
{
// Load from database
}
public async Task<IReadOnlyList<FeatureFlagDefinition>> GetAllAsync(CancellationToken ct = default)
{
// Load all from database, ordered by name
}
public async IAsyncEnumerable<FeatureFlagChange> WatchAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
// Poll for changes, use database notifications, etc.
}
}

Note: FeatureFlagEvaluator is internal to the Pragmatic.FeatureFlags package. Custom stores that want to reuse the built-in rule evaluation logic should either: (a) duplicate the evaluation logic, or (b) store definitions and delegate IsEnabledAsync to loading the definition and evaluating rules manually.

DecisionRationale
Separate from IConfigurationStoreFeature flags need context-based evaluation, not simple key-value lookup
Unknown flags return falseSafe default — unrecognized flags are disabled
Case-insensitive flag namesPrevents subtle bugs from casing differences
IAsyncEnumerable for WatchAsyncNatural streaming pattern, no callback registration needed
Store is singleton by defaultFlag definitions are global; evaluation context comes from the caller