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.
IFeatureFlagStore Interface
Section titled “IFeatureFlagStore Interface”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);}| Method | Description |
|---|---|
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. |
InMemoryFeatureFlagStore
Section titled “InMemoryFeatureFlagStore”The default store for development and testing. Uses ConcurrentDictionary<string, FeatureFlagDefinition> with case-insensitive keys.
Registration
Section titled “Registration”// Default: in-memory storeservices.AddPragmaticFeatureFlags();Additional Methods
Section titled “Additional Methods”Beyond IFeatureFlagStore, the in-memory store exposes:
| Method | Description |
|---|---|
Define(FeatureFlagDefinition) | Create or update a flag definition |
Remove(string flagName) | Remove a flag definition. Returns true if the flag existed. |
Evaluation
Section titled “Evaluation”The InMemoryFeatureFlagStore delegates to FeatureFlagEvaluator (internal), which processes rules in order and falls back to the global Enabled state if no rule matches.
Change Notification
Section titled “Change Notification”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.
DI Registration Options
Section titled “DI Registration Options”// Default: in-memory store (via TryAdd)services.AddPragmaticFeatureFlags();
// Custom store typeservices.AddPragmaticFeatureFlags<MyDatabaseFeatureFlagStore>();
// Manual registration (register before AddPragmaticFeatureFlags)services.AddSingleton<IFeatureFlagStore, MyCustomStore>();services.AddPragmaticFeatureFlags(); // TryAdd won't replace your storeThe generic overload AddPragmaticFeatureFlags<TStore>() uses AddSingleton (not TryAdd), so it always replaces any previous registration.
Implementing a Custom Store
Section titled “Implementing a Custom Store”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.
Key Design Decisions
Section titled “Key Design Decisions”| Decision | Rationale |
|---|---|
Separate from IConfigurationStore | Feature flags need context-based evaluation, not simple key-value lookup |
Unknown flags return false | Safe default — unrecognized flags are disabled |
| Case-insensitive flag names | Prevents subtle bugs from casing differences |
IAsyncEnumerable for WatchAsync | Natural streaming pattern, no callback registration needed |
| Store is singleton by default | Flag definitions are global; evaluation context comes from the caller |