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:
| Rule | Example |
|---|---|
| Per user / segment | userId in ["user-1", "user-2"] |
| Per attribute | accountTier == "enterprise" |
| By percentage | 25% (stable across calls per userId) |
| Date-windowed | Enabled between 2026-06-01 and 2026-07-01 |
| Geographic | country in ["US", "CA"] |
| Version-gated | app_version >= "2.4.0" |
| Allow/block lists | userId 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% → everyoneAt each step:
- Watch error rates, latency, conversion, anything the change could move.
- One change at a time. Don't ramp two features in parallel — you won't know which caused the issue.
- Bucket on a stable ID (user, org) so a user's experience doesn't flicker between variants.
- 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 fallbackConventions:
- Default
truein 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_enabledto 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:
| Environment | Default behavior |
|---|---|
| Local / dev | All release flags on by default — devs work against the latest |
| Staging | Match production with optional QA exceptions |
| Production | Default 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 devThis 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.