For years, deployment and release were the same thing: merge to main, build to production, and hope nothing blows up. The problem is that shipping code and exposing functionality are different decisions. A deploy can be flawless and the feature can still be a disaster for users. Feature flags separate both decisions: code reaches production switched off, and you turn it on when you decide, for whoever you decide.
This separation is the foundation of progressive delivery: release to 5% of users, watch the metrics, raise to 25%, validate, and so on to 100%. And if something breaks, a kill switch turns the feature off in seconds — no rollback, no redeploy.
1. Deploy ≠ Release
With flags, every merge to main lands in production in a dormant state. This enables true trunk-based development: short-lived branches, frequent continuous integration, and zero long-lived branches that rot. Incomplete code gets integrated behind a disabled flag; the feature is "released" later with a configuration change.
- Deploy: moving code to production. Frequent, boring, automated.
- Release: exposing the functionality to users. Controlled, gradual, reversible.
2. Evaluation with fallback: a flag should never take the app down
A flag is just another dependency, and every dependency can fail. If your flag provider stops responding, your application must degrade to the default value, not hang. The rule: every evaluation call has a short timeout and an explicit fallback.
// Evaluating a flag with a safe fallback
function isEnabled(flag, ctx) {
try {
return client.getBooleanDetail(flag, ctx).enabled;
} catch (err) {
// If the provider goes down, the flag falls back
// to its default state
return defaults[flag] ?? false;
}
}
if (isEnabled('checkout-v2', { userId, region })) {
return renderCheckoutV2();
}
return renderCheckoutV1();
The default value must be the safe behavior: false for a risky feature; for a kill switch protecting against a fragile external dependency, probably the pre-change behavior.
3. Percentage-based progressive rollout
The core pattern of progressive delivery is the percentage rollout: the flag enables the feature for a percentage of users that grows based on what the metrics say. The technical key is deterministic bucketing: the same user must always land in the same bucket, on any instance, at any time.
// Percentage-based progressive rollout (stable bucketing)
function inRollout(flag, userId) {
const cfg = config.get(flag); // { enabled, percentage }
if (!cfg?.enabled) return false;
// Deterministic hash: the same user always lands
// in the same bucket, on any replica.
const bucket = hash(flag + ':' + userId) % 100;
return bucket < cfg.percentage; // 5 -> 25 -> 50 -> 100
} - Deterministic hash: without it, a user sees the feature and then doesn't; support bears the pain.
- Segmentation: beyond percentage, filter by region, plan, or internal users before the public rollout.
- Advancement criteria: error rate, p95 latency, and conversion defined before raising the percentage, not after.
4. Kill switches: the life insurance of features
Not every flag exists to launch new features. A kill switch wraps a fragile dependency or a sensitive behavior change and lets you disable it instantly. It's the difference between a 2-minute incident and a 40-minute one: rolling back a flag is a configuration change; rolling back a deploy means rebuilding, redeploying, and praying the data migration doesn't get in the way.
5. Flag debt is real
Feature flags accumulate like configuration parameters. Every living flag doubles execution paths that someone must maintain and test. The discipline that prevents chaos:
- Name and owner: every flag declares which feature it belongs to, who created it, and when it expires.
- Expiry date: if a flag has been on at 100% for 90 days, the next ticket is deleting it.
- Audits: a periodic inventory of active flags, with alerts for abandoned ones.
- Separate types: don't mix release flags (temporary) with configuration flags (permanent) in the same system or naming convention.
Summary
Feature flags turn releasing into a reversible, gradual decision instead of an act of faith. They decouple deploy from release, make trunk-based development viable, and give you a kill switch when the metrics say stop. Start with a single critical flow, measure, and remove every flag as soon as it fulfills its mission: progressive delivery isn't a tool, it's an engineering habit.