Steven's Knowledge

Patterns

Targeting rules, gradual rollout, kill switches, A/B testing, dependent flags, environment management

Patterns

The same flag mechanism powers radically different uses. The patterns below cover the ones you'll reach for repeatedly.

Targeting Rules

A flag isn't binary — it's an expression evaluated against context:

context: { userId, country, accountTier, deviceType, app_version, ... }

Common rules:

RuleExample
Per user / segmentuserId in ["user-1", "user-2"]
Per attributeaccountTier == "enterprise"
By percentage25% (stable across calls per userId)
Date-windowedEnabled between 2026-06-01 and 2026-07-01
Geographiccountry in ["US", "CA"]
Version-gatedapp_version >= "2.4.0"
Allow/block listsuserId not in <blocklist>

Most platforms let you combine rules; the first matching rule wins.

Gradual Rollout

The canonical use of flags:

day 1:  internal users only          → 20 people
day 2:  add the beta program          → 500 people
day 3:  5% of all traffic             → ~5,000 users
day 5:  25%                           → ~25,000
day 7:  50%                           → ~50,000
day 10: 100%                          → everyone

At each step:

  1. Watch error rates, latency, conversion, anything the change could move.
  2. One change at a time. Don't ramp two features in parallel — you won't know which caused the issue.
  3. Bucket on a stable ID (user, org) so a user's experience doesn't flicker between variants.
  4. Roll forward, not back when you see regressions — but be ready to flip the flag off instantly.

Bucketing Correctly

hash(userId) % 100 < rollout_pct is the standard. Two properties matter:

  • Stable: the same user always gets the same answer.
  • Uncorrelated: the bucket for flag A is independent of flag B.

Most platforms handle this for you. Don't roll your own unless you really need to.

Kill Switches

Long-lived "off if things break" flags around critical paths:

if (await flags.isEnabled('recommendations_enabled', { default: true })) {
  return await recommendationService.get(userId);
}
return [];   // graceful fallback

Conventions:

  • Default true in code — they only matter when flipped off.
  • Wrap the slowest / most fragile dependencies — recommendations, search ranking, third-party APIs.
  • Document in the runbook: "If recommendations are degrading SLOs, set recommendations_enabled to false."
  • Don't expire. They're permanent ops controls, not release flags.

The cost is one extra branch in production code; the benefit is a 30-second remediation instead of a 30-minute deploy during an incident.

Experiments / A/B Testing

A flag that picks between variants, tied to an analytics event:

const variant = await flags.getVariant('pricing_experiment', {
  userId: user.id,
});

const price = variant.name === 'control'
  ? 9.99
  : variant.name === 'higher'
    ? 14.99
    : 12.99;

// Log impression to your analytics tool
analytics.track('pricing_seen', { variant: variant.name, userId: user.id, price });

// Later: did the variant convert?
analytics.track('purchase_completed', { variant: variant.name, userId: user.id, price });

Statistical correctness matters here:

  • Pre-register your hypothesis and sample size. Peeking at results mid-experiment biases conclusions.
  • Run for at least one full business cycle (a week minimum for weekly patterns).
  • Mind sample ratio mismatch (SRM) — if 50/50 split assigns 47/53, something is broken (caching, bot traffic, exposure logic).
  • Mind interactions — running two experiments on the same user simultaneously can confound results.

For serious experimentation, use a platform built for it (Statsig, LaunchDarkly Experiments, Split, GrowthBook). A naive flag tool won't compute confidence intervals or run sequential testing.

Dependent Flags

Sometimes flag B is meaningful only when flag A is on. Most platforms support this as a first-class concept:

Flag: new-checkout-step-2
  Depends on: new-checkout is enabled
  Strategy: gradual rollout 50%

Without dependency support, you express it in code:

if (await flags.isEnabled('new-checkout', ctx) &&
    await flags.isEnabled('new-checkout-step-2', ctx)) {
  // ...
}

Use sparingly — deep dependency chains are hard to reason about. Two levels is fine; five is a smell.

Environment Management

Most platforms support per-environment values: a flag can be on in dev, 25% in staging, off in production with no separate config files.

The discipline:

EnvironmentDefault behavior
Local / devAll release flags on by default — devs work against the latest
StagingMatch production with optional QA exceptions
ProductionDefault off for unreleased flags; precise rollout config

A common trap: a flag is on in staging but the production rollout setting was never configured. The flag is off in production by default; the feature looked fine in staging but never reached real users. Build alerts for "flag is on in lower env but missing in production."

Multi-Variant Flags

A flag isn't always boolean. Multi-variant flags return one of N strings:

flag: cta_button_text
variants:
  control:     "Sign Up"           weight 50%
  variant_a:   "Get Started Free"  weight 25%
  variant_b:   "Start in 30s"      weight 25%
const variant = await flags.getString('cta_button_text', 'Sign Up', ctx);

The same shape powers experiments, multi-tenant theming, regional content variations, and progressive rollout of complex changes (not just on/off).

Local Override for Development

Devs want to flip flags without touching production config. Most SDKs support:

const unleash = initialize({
  url: '...',
  bootstrap: {
    data: [{ name: 'new-checkout', enabled: true, strategies: [{ name: 'default' }] }],
  },
  // Local overrides win over server values during development
});

Or environment variables:

UNLEASH_BOOTSTRAP_NEW_CHECKOUT=true npm run dev

This lets QA reproduce production-only behavior, and dev work proceed offline.

What's Next

You can use flags for release, experimentation, ops, and entitlement. Best Practices covers managing flag debt, fail-safe defaults, latency, and the operational side.

On this page