Skip to content
bitzorcas
中EN

Concept

Feature Flags

Manage capability rollout with global feature definitions, tenant overrides, authorization evaluation, and cache invalidation.

Last updated

BitzOrcas has a database-backed Feature implementation. A Feature is not a Boolean scattered through application code; it is a runtime capability composed of a global definition catalog, tenant overrides, authorization evaluation, and caching.

Decision model

Feature code
↓
Tenant override exists? ── yes → use OverrideState
│ no
└────────────────────→ use global DefaultState

FeatureStore reads global definitions from SysFeatureDefinition and tenant overrides from SysFeatureOverride. A missing definition or storage failure evaluates as disabled, preventing an infrastructure failure from opening a capability.

yesno

Module Feature declaration

Global DefaultState

Tenant override exists?

OverrideState

DefaultState

Authorization Feature evaluator

Allow / Deny / Neutral

Defining a Feature

A module declares stable feature codes in its own contract boundary and describes them with FeatureDefinition metadata. A source generator collects the definitions at compile time; runtime assembly scanning is not required.

// ① Focus on the contract and control flow; keep validation, cancellation, and typed errors explicit.
[FeatureCatalog]
public static class TicketFeatures
{
[FeatureDefinition("Ticket management", DefaultEnabled = false)]
public const string Ticketing = "platform.tickets";
}

Once a code is referenced by plans, tenant overrides, or audit records, treat it as a compatibility contract. Renaming it requires data and consumer migration, not just a constant edit.

Evaluation in authorization

FeaturePolicyEvaluator maps the protected resource’s module to a feature code and asks IFeatureDecisionProvider for the current tenant state. Enabled produces Allow, disabled produces Deny, and resources without a Feature constraint produce Neutral so other evaluators can decide.

Avoid repeating if (featureEnabled) in every handler. Evaluating at the authorization boundary is more consistent and makes missing checks less likely.

Current enforcement coverage

FeaturePolicyEvaluator.ModuleFeatureMap currently contains only three mappings:

Resource moduleFeature codeDisabled result
ticketsplatform.ticketsDeny
chatplatform.chatDeny
workflowplatform.workflowDeny
every other moduleno mappingNeutral

Website, Documents, Webhooks, Search, and other modules declare Features, but declaration does not automatically place them in the generic evaluator. Every new declaration needs a tested enforcement point: a mapping, module policy, endpoint filter, Job/Consumer guard, or another explicit boundary.

Plans, entitlements, and rollout switches

PlatformBilling.EntitlementResolver can consume FeatureStore and catalog mappings, but FeatureStore itself has only tenant override then global default. It does not automatically merge plan JSON, License Edition, rollout percentage, or a user cohort.

commercial plan/license → determines what a tenant should own
Feature management → stores global default and tenant override
authorization evaluator → enforces mapped resource Allow/Deny
rollout switch → controls progressive exposure and rollback

If plan synchronization writes tenant overrides, make the sync idempotent, revocable, and audited. A rollout experiment also needs an owner, end date, and cleanup plan.

Managing state

The management surface provides Feature query and update use cases. Updates depend on definition scope:

  • Tenant-scoped Feature: upsert an override for the current tenant without changing the global default.
  • Platform-scoped Feature: update global DefaultState.

After a successful update, the application publishes FeatureChangedIntegrationEvent and invalidates the tenant Feature cache. Override rows store only the changed state; deleting one naturally restores the global default.

For a platform-scoped Feature, the current event and invalidation still carry the operator’s current tenant. Prove that a global state change invalidates every affected tenant; invalidating only one tenant can leave others stale until TTL.

Cache and failure semantics

The evaluator uses auth:feature:{tenantId}:{featureCode}, a Medium TTL, and a tenant Feature tag. Cached enabled returns Allow, cached disabled returns Deny, and an unmapped module returns Neutral.

// Disabled is an explicit Deny; only an unknown/unmapped resource is Neutral.
var decision = await evaluator.EvaluateAsync(
currentUser, resource, action, cancellationToken);
// Assert evaluator semantics, not only the final HTTP status.
decision.Evaluator.ShouldBe(nameof(FeaturePolicyEvaluator));

FeatureStore catches non-cancellation failures and returns false. Unavailable providers also remain closed. A cache failure may follow cache-port fallback, but source failure must not become enabled.

Test matrix

  • default state applies without an override; Enabled and Disabled overrides win;
  • missing definition, store exception, and unavailable adapter remain closed;
  • disabled tickets/chat/workflow produces explicit Deny;
  • an unmapped module produces Neutral for other policies to decide;
  • tenant A does not change tenant B; global change handles every tenant cache;
  • API, background jobs, and consumers agree on disabled behavior;
  • repeated FeatureChanged consumption is safe and audit retains actor/transition.

Rollout policy

Features work well for observable, reversible rollout, such as enabling a module for internal tenants before widening access. They are a poor excuse for preserving two business models indefinitely. Once a rollout stabilizes, remove the old branch and decide whether the code remains as a commercial entitlement boundary.

Before release, verify:

  1. The code appears in the definition catalog and seed data.
  2. The default follows least privilege.
  3. An override for tenant A cannot affect tenant B.
  4. Disabling the Feature has consistent effects across APIs, jobs, and asynchronous consumers.
  5. The change event, cache invalidation, and audit trail are observable.

Local and test environments

Without the production persistence adapter, UnavailableFeatureStore and UnavailableFeatureDecisionProvider preserve closed behavior. Tests should provide explicit states rather than depend on ambient configuration. Cover global enabled, global disabled, tenant override, and storage failure paths.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%