Skip to content

Flag Taxonomy

Flags are modeled as three primitive value shapes plus composable traits — not as a separate evaluation code path per “kind” like kill switch or A/B test. A “kind” is schema sugar over the same FlagDefinition shape.

Shape Description Example
boolean On/off release flag, kill switch
variant One of N named, weighted variants A/B/C test
value Arbitrary typed payload dynamic config value

These fields live directly on FlagDefinition and FlagRemoteState:

Field Source What it does
failureMode definition 'closed' | 'open' | 'lastKnown' — outcome when the provider fails to fetch remote state
rolloutPercentage remote state Percentage-based bucketing, see Bucketing
targetingRules remote state Attribute/rule-based matching (TargetingRule[]), see Targeting Rules
sticky definition Whether the same bucketing key always resolves the same way
emitsExposure definition Fires every subscribed onEvaluate handler on FlagsClient whenever a flagged requested read resolves
schedule definition Optional { startAt?, endAt? } time window
dependsOn definition Parent flag key(s) — this flag only evaluates if every parent is truthy

failureMode is enforced by FlagsClient, not the pure evaluate() function — a bare evaluate() call always uses whatever remote state you hand it, with no provider-failure concept to react to. When a FlagsClient-owned provider fetch fails:

  • 'closed' (the default): falls back to the definition’s defaultValue, reason: 'fallbackError'.
  • 'open': a boolean-shaped flag fails open (value: true) instead of falling back to defaultValue — useful for flags that gate a fail-safe (e.g. “allow legacy checkout”) rather than a new feature.
  • 'lastKnown': if a previous successful fetch is cached, re-evaluates against that stale remote state (stale: true) instead of falling back.

emitsExposure is wired to FlagsClient.onEvaluate(): every handler registered there fires on a requested (not dependsOn-internal) read of a flag with emitsExposure: true. Overridden reads (via setOverrides()) never fire exposure handlers, since a forced test read isn’t a real user exposure.

FlagDefinition.kind is one of:

type FlagKind =
| "release" | "killSwitch" | "experiment" | "migration"
| "progressiveDeploy" | "entitlement" | "circuitBreaker"
| "dynamicConfig" | "custom";

kind is a plain string field — it’s metadata for your own bookkeeping/tooling, not something evaluate() branches on itself.

@useoptimus/core ships 8 factory functions that pre-fill trait defaults for a given kind over the same FlagDefinition shape, rather than a separate evaluation code path per kind. Building a FlagDefinition object directly (see Quick Start) still works identically — every factory below is convenience, not a different code path.

Boolean, closed fail-safe, no special traits — the default choice for a plain feature flag:

import { defineReleaseFlag } from '@useoptimus/core';
const showNewNav = defineReleaseFlag({ key: 'show-new-nav', defaultValue: false });

Boolean, closed fail-safe — meant to be flipped fast and manually:

import { defineKillSwitch } from '@useoptimus/core';
const maintenanceMode = defineKillSwitch({ key: 'maintenance-mode', defaultValue: false });

Variant-shaped, sticky, emits exposure events for analytics:

import { defineExperiment } from '@useoptimus/core';
const checkoutExperiment = defineExperiment({
key: 'checkout-v2',
defaultValue: 'control',
variants: [
{ key: 'control', value: 'control', weight: 50 },
{ key: 'treatment', value: 'treatment', weight: 50 },
],
});

Boolean, sticky, closed fail-safe — pairs with runShadow/ runShadowAsync for comparing an old and new code path; see Migrations & Shadow Mode for the full worked example:

import { defineMigrationFlag } from '@useoptimus/core';
const useNewPricingEngine = defineMigrationFlag({ key: 'use-new-pricing-engine', defaultValue: false });

Boolean, sticky — “rings”/waves are a rollout convention realized via FlagRemoteState.rolloutPercentage over time, not a structural feature of this factory:

import { defineProgressiveDeploy } from '@useoptimus/core';
const newBillingPipeline = defineProgressiveDeploy({ key: 'new-billing-pipeline', defaultValue: false });

Boolean, closed fail-safe — intended to be driven by targetingRules (plan tier, org, role) rather than random rollout:

import { defineEntitlementFlag } from '@useoptimus/core';
const advancedReporting = defineEntitlementFlag({ key: 'advanced-reporting', defaultValue: false });

Boolean, closed fail-safe — intended to be flipped programmatically (e.g. by a monitoring system), not manually:

import { defineCircuitBreaker } from '@useoptimus/core';
const disableThirdPartyEnrichment = defineCircuitBreaker({ key: 'disable-third-party-enrichment', defaultValue: false });

Arbitrary typed value, targeting-rules-driven — no boolean semantics at all:

import { defineDynamicConfig } from '@useoptimus/core';
const maxUploadSizeMb = defineDynamicConfig<number>({ key: 'max-upload-size-mb', defaultValue: 25 });