Feature Flags and Progressive Delivery: Ship Without Fear · Daniel Tinizaray

Feature Flags and Progressive Delivery: Ship Without Fear · Daniel Tinizaray

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.

⚠️ Golden rule If new and old code coexist behind a flag, make sure both paths write compatible data. A flag doesn't protect you from an irreversible schema migration: design data changes to be expand-contract.

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.


Enjoyed this article?

If you're dealing with these challenges in your company, let's talk. No obligation. 30 minutes to understand your situation.

Book a free call →