The Feature Flags pattern enables activating or deactivating features at runtime
without redeployment. Granit extends this pattern for SaaS tiering: feature
resolution follows a multi-level cascade Tenant > Plan > Default , with a
FusionCache L1/L2 cache for performance.
Three feature types are supported:
Toggle : enabled/disabled (boolean)
Numeric : numeric value with min/max constraints (SaaS quotas)
Selection : value from an allowed set of choices
sequenceDiagram
participant API as Endpoint / Handler
participant FC as FeatureChecker
participant HC as FusionCache (L1+L2)
participant TVP as TenantValueProvider (20)
participant PVP as PlanValueProvider (10)
participant DVP as DefaultValueProvider (0)
participant FS as IFeatureStore (DB)
API->>FC: GetValueAsync("MaxUsers")
FC->>HC: GetOrSetAsync("t:{tid}:MaxUsers")
alt Cache hit
HC-->>FC: Cached value
else Cache miss
HC->>TVP: GetValueAsync(feature, tenantId)
TVP->>FS: Read tenant override
alt Override found
FS-->>TVP: "500"
TVP-->>HC: "500"
else No override
TVP-->>HC: null
HC->>PVP: GetValueAsync(feature, planId)
PVP-->>HC: "100" (plan value)
end
HC-->>HC: Store in L1 + L2
end
FC-->>API: "500" or "100"
Component File Role FeatureDefinitionsrc/Granit.Features/Definitions/FeatureDefinition.csName, default value, type, constraints FeatureDefinitionProvidersrc/Granit.Features/Definitions/FeatureDefinitionProvider.csAbstract class to be implemented by the application FeatureDefinitionStoresrc/Granit.Features/Definitions/FeatureDefinitionStore.csSingleton registry aggregating all providers FeatureGroupDefinitionsrc/Granit.Features/Definitions/FeatureGroupDefinition.csLogical grouping of features
Provider Order File Source TenantFeatureValueProvider20 src/Granit.Features/ValueProviders/TenantFeatureValueProvider.csIFeatureStore (DB)PlanFeatureValueProvider10 src/Granit.Features/ValueProviders/PlanFeatureValueProvider.csIPlanFeatureStore (application)DefaultValueFeatureValueProvider0 src/Granit.Features/ValueProviders/DefaultValueFeatureValueProvider.csFeatureDefinition.DefaultValue (code)
Component File Role FeatureCheckersrc/Granit.Features/Internal/FeatureChecker.csResolution orchestration + IFusionCache FeatureCacheKeysrc/Granit.Features/Cache/FeatureCacheKey.csFormat: t:{tenantId}:{featureName} FeatureCacheInvalidationHandlersrc/Granit.Features/Cache/FeatureCacheInvalidationHandler.csListens to FeatureValueChangedEvent, expires cache entries
Component File Role IFeatureLimitGuardsrc/Granit.Features/Limits/IFeatureLimitGuard.csCheckAsync(feature, currentCount) — throws FeatureLimitExceededExceptionFeatureLimitGuardsrc/Granit.Features/Limits/FeatureLimitGuard.csImplementation
The core is framework-pure; gating ships in separate binding packages (see
ADR-062 ).
MVC controller support was removed — Granit is Minimal-API-first.
Component File Role .RequiresFeature()src/Granit.Http.Features/AspNetCore/FeatureEndpointConventionBuilderExtensions.csMinimal-API endpoint filter (Granit.Http.Features) RequiresFeatureEndpointFiltersrc/Granit.Http.Features/AspNetCore/RequiresFeatureEndpointFilter.csReturns 403 when the feature is disabled [RequiresFeature]src/Granit.Features.Wolverine/Attributes/RequiresFeatureAttribute.csAttribute on the message type (Granit.Features.Wolverine) RequiresFeatureMiddlewaresrc/Granit.Features.Wolverine/RequiresFeatureMiddleware.csWolverine pipeline middleware
Problem Solution Different SaaS plans (Free/Pro/Enterprise) with different limits Numeric features with NumericConstraint + FeatureLimitGuardPer-tenant override without redeployment TenantFeatureValueProvider reads overrides from DBPerformance: resolution must not query the DB on every request FusionCache L1 (in-memory) + L2 (Redis) with event-driven invalidation Multi-instance consistency: a feature change must be visible everywhere FeatureValueChangedEvent expires cache entries via ExpireAsync + backplaneAPI protection: block access if the feature is disabled .RequiresFeature() on Minimal-API endpoints (Granit.Http.Features) + [RequiresFeature] on Wolverine message types (Granit.Features.Wolverine)
// 1. Define features (code-first)
public sealed class AcmeFeatureDefinitionProvider : FeatureDefinitionProvider
public override void Define (IFeatureDefinitionContext context)
FeatureGroupDefinition group = context . AddGroup ( " Acme " );
group . AddFeature ( " Acme.MaxUsers " ,
valueType: FeatureValueType . Numeric ,
numericConstraint: new NumericConstraint(Min: 1 , Max: 10_000 ));
group . AddFeature ( " Acme.Telehealth " ,
valueType: FeatureValueType . Toggle );
public static class CreatePatientHandler
public static async Task Handle (
CreatePatientCommand command,
IFeatureLimitGuard limitGuard,
IFeatureChecker features,
CancellationToken cancellationToken)
// Throws FeatureLimitExceededException if quota is reached
long currentCount = await db . Patients . CountAsync (ct);
await limitGuard . CheckAsync ( " Acme.MaxUsers " , currentCount, ct);
// Check that a feature toggle is enabled
await features . RequireEnabledAsync ( " Acme.Telehealth " , ct);
// 3. Protect an endpoint
app . MapPost ( " /api/patients " , CreatePatientEndpoint . Handle )
. RequiresFeature ( " Acme.MaxUsers " );