Promoted to Evolith: This ADR has been elevated to Evolith ADR-0048 — Feature Flag System Scope and Structured Criteria Model. UMS retains this document as implementation reference.
Status: Accepted Date: 2026-05-27 Decision Owner: Architecture Related:
The original FeatureFlag aggregate was designed as a global toggle: FlagCode was unique across the entire platform without any ownership scope. This created two operational problems:
No system isolation. A flag activated globally could affect tenants or system suites that should not be included in a specific rollout. There was no mechanism to constrain a flag to a particular system boundary.
Rigid targeting model. The original FlagTargets property was a free-form JSON string that described targeting rules. This made it impossible to query, validate, or evolve targeting conditions without parsing opaque payloads. Adding or removing a single targeting condition required replacing the entire JSON blob.
The Authorization bounded context (BC-B) already models SystemSuite as the authoritative unit of feature composition — a system suite groups modules, roles, and permissions for a specific product or system. Feature flags naturally belong to the same boundary: a flag for “enable new export module” is only meaningful within the suite that contains the export module.
A refactoring was needed to introduce SystemSuite as the mandatory scope of every feature flag and to replace the opaque targeting JSON with a structured, queryable criteria model.
FeatureFlag remains an Aggregate Root in the Configuration bounded context (ums_configuration schema). It is not converted into a child entity of the SystemSuite aggregate in BC-B. The link between the two aggregates is expressed through a foreign key: FeatureFlag.SystemSuiteId → ums_authorization.SystemSuites.Id.
Every FeatureFlag must be created with a valid SystemSuiteId. This value is validated against BC-B at creation time and is immutable thereafter. A flag cannot be transferred from one system suite to another; if a different scope is needed, a new flag must be created.
The previous global unique constraint on FlagCode is replaced by a composite unique constraint on (SystemSuiteId, FlagCode). The same code string may exist in different system suites without conflict.
A new owned entity FeatureFlagCriteria replaces the free-form FlagTargets JSON for targeting purposes. Each criterion carries:
CriteriaType — the dimension being evaluated (TenantId, BranchId, UserProfileId, RoleCode, Environment, DateRange, PercentageHash, CustomRule)Operator — the comparison to apply (Equals, NotEquals, In, Between, LessThanOrEqual, Matches)Value — the target value as a JSON-compatible stringThe criteria collection is optional and dynamic:
The IFeatureFlagEvaluator port evaluates the criteria collection using the following rules:
If the evaluation context does not provide the data required by a criterion, the evaluation returns false (safe posture). This prevents inadvertent feature activation when context is partially populated.
Converting FeatureFlag into a child entity of the SystemSuite aggregate was considered but rejected for four reasons:
Aggregate size. SystemSuite already owns modules, roles, and permission templates. Adding an unbounded collection of feature flags would grow the aggregate to an unmanageable size, harming load times and increasing the risk of concurrency conflicts.
Independent lifecycle. Feature flags transition through their own states (Inactive → Active → Archived) on a schedule driven by release management, not by the system suite lifecycle. Archiving a flag does not affect the system suite; disabling a system suite does not archive its flags.
Existing pattern. AppConfiguration and IdpConfiguration in BC-C demonstrate that configuration aggregates reference BC-B and BC-A identifiers as FKs without becoming children of those aggregates. FeatureFlag follows the same established pattern.
Bounded context separation. Feature flag management is a configuration responsibility (BC-C). Moving it into BC-B would merge two distinct subdomains and violate the single-responsibility principle at the bounded context level.
The opaque FlagTargets JSON made it impossible to:
A structured FeatureFlagCriteria entity collection addresses all four concerns at the cost of a slightly more complex schema.
An absent context value most commonly indicates an anonymous caller, a service request without tenant context, or a client that has not yet migrated to provide the required context field. In all cases, activating a targeted feature for an unidentified caller would be incorrect. The false-on-missing-context rule ensures that new criteria types can be added to a live flag without risk of unintended broad activation.
IFeatureFlagEvaluator port keeps evaluation logic extensible and testable in isolation.ums_configuration → ums_authorization) introduces a database-level coupling between two bounded contexts. This is an intentional constraint enforcing referential integrity at the persistence layer.SystemSuiteId, which means the caller must resolve the suite identity before issuing the command.FeatureFlagCriteria) and requires JOIN operations for full flag reads. A read-model projection is recommended for high-frequency evaluation paths.FeatureFlag would become an entity inside the SystemSuite aggregate, managed through SystemSuite commands.
Rejected. As argued in the Rationale section, this bloats the SystemSuite aggregate, couples two different lifecycle boundaries, and violates bounded context separation. The independent aggregate pattern is already established for other configuration entities in BC-C.
Replace the internal domain model with an external SaaS feature flag provider and expose it through the IFeatureFlagPort anti-corruption layer already defined in the bounded context map.
Out of scope. The existing IFeatureFlagPort ACL entry in the bounded context map accounts for this possibility. The decision to keep feature flags internal is a product scope decision, not an architectural one. This ADR governs the internal model design; the external integration path remains available through the ACL port without requiring changes to this decision.
| Concern | Location |
|---|---|
| Aggregate root | Ums.Domain.Configuration.FeatureFlag |
| Criteria entity | Ums.Domain.Configuration.FeatureFlagCriteria |
| Evaluator port | Ums.Domain.Configuration.Ports.IFeatureFlagEvaluator |
| Evaluator implementation | Ums.Infrastructure.Configuration.FeatureFlagEvaluator |
FeatureFlags table |
ums_configuration.FeatureFlags — ADD SystemSuiteId FK, TenantId; CHANGE UK |
FeatureFlagCriteria table |
ums_configuration.FeatureFlagCriteria (new table) |
| New commands | AddFeatureFlagCriteriaCommand, RemoveFeatureFlagCriteriaCommand, UpdateFeatureFlagCommand |
| Modified commands | CreateFeatureFlagCommand (+ SystemSuiteId, TenantId), EvaluateFeatureFlagCommand (typed EvaluationContext) |
| New queries | GetFeatureFlagsBySystemSuiteQuery, GetFeatureFlagCriteriaQuery |
| ADR Registry | FeatureFlag Domain | FeatureFlagCriteria |