How do feature flags enable progressive delivery, and how do you actually implement one cheaply?
Feature flags decouple deploying code from releasing it to users, and a solid percentage-rollout flag is nothing more than a deterministic hash and a comparison.
LEARN BY DOINGPut your intuition to work3 steps
How do feature flags enable progressive delivery, and how do you actually implement one cheaply?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Deploy is not release
Most teams learn to say "feature flags decouple deploy from release" long before they understand what that buys them. Deploying is a purely technical act: new code lands on machines and starts running. Releasing is a business decision: which users get to see or use that code, and when. Without a flag, those two events are forced to happen at the exact same instant — the moment your CI/CD pipeline finishes, every user on earth gets the new behavior, whether or not it is finished, whether or not your on-call engineer is awake, and whether or not marketing has announced anything. A feature flag is just an if statement backed by a runtime-configurable value, but that one seam is what lets you ship code continuously while still controlling exposure independently.
Progressive delivery is what you build on top of that seam. Instead of a binary on/off, you expose the new code path to a small, growing slice of real traffic and watch real metrics before going further. The most common shapes are:
- Percentage rollout — expose the flag to 1%, then 5%, then 25%, then 100% of users, watching error rates and latency at each step.
- Ring / segment targeting — internal employees first, then beta opt-ins, then a specific region or plan tier, then everyone.
- Canary by infrastructure — a small number of servers run the new version rather than a percentage of users; useful when the risk is operational (memory leak, CPU regression) rather than behavioral.
- Kill switch — a flag with no gradual rollout at all, whose only job is to be flipped off in seconds if something goes wrong, without a redeploy.
That last one is worth dwelling on, because it is the cheapest, highest-leverage flag you can add: wrapping a risky new integration or a call to a flaky third-party API in a flag that defaults to "on" but can be forced to "off" turns an incident that requires a hotfix deploy into an incident that requires flipping a boolean. Interviewers listen for whether you understand this distinction between release flags (temporary, meant to be deleted once the feature is fully rolled out) and ops/permission flags (long-lived, part of the system's actual control surface).
#Building one without buying a platform
You do not need LaunchDarkly, Unleash, or Split to get real value from flags — you need one property that a lot of home-grown implementations get wrong: stickiness. If a flag decides per-request whether a user sees the new checkout flow, that user can flip between old and new on every page load, which corrupts your metrics and produces a genuinely broken experience. The fix is to make the bucket assignment a deterministic function of something stable — the user ID — rather than a fresh coin flip:
import { createHash } from "crypto";type FlagConfig = {key: string;enabled: boolean;rolloutPercentage: number; // 0-100allowList?: string[];denyList?: string[];};class FeatureFlagService {private flags = new Map<string, FlagConfig>();constructor(flags: FlagConfig[]) {for (const flag of flags) this.flags.set(flag.key, flag);}isEnabled(flagKey: string, userId: string): boolean {const flag = this.flags.get(flagKey);if (!flag || !flag.enabled) return false;if (flag.denyList?.includes(userId)) return false;if (flag.allowList?.includes(userId)) return true;return this.bucketFor(flagKey, userId) < flag.rolloutPercentage;}// Same user + same flag always lands in the same 0-99 bucket.// That determinism is what makes a rollout monotonic: raising// rolloutPercentage from 10 to 20 can only ADD users to the// "on" group, never flip an already-enabled user back to "off".private bucketFor(flagKey: string, userId: string): number {const digest = createHash("sha256").update(`${flagKey}:${userId}`).digest();return digest.readUInt32BE(0) % 100;}updateRollout(flagKey: string, percentage: number): void {const flag = this.flags.get(flagKey);if (flag) flag.rolloutPercentage = Math.max(0, Math.min(100, percentage));}}const flags = new FeatureFlagService([{ key: "new-checkout-flow", enabled: true, rolloutPercentage: 10 },]);if (flags.isEnabled("new-checkout-flow", "user-8842")) {// render the new checkout} else {// render the legacy checkout}
This is the entire mechanism behind every commercial flagging platform's percentage rollout, plus a config table and an admin UI. A single hash function, a modulo, and a comparison — cheap enough to write in an afternoon, and correct enough to trust in production, as long as the hash input includes both the flag key (so a user isn't in the same relative bucket for every flag, which would make different rollouts correlated with each other) and the user ID (so the assignment is stable per user).
#The flag you forget to delete
Flags are cheap to add and expensive to leave behind. Every conditional you introduce doubles the number of code paths your team has to reason about, and a codebase with two hundred long-lived flags is a codebase where nobody is confident which combination of flags is actually running in production. The discipline that separates teams who benefit from flags and teams who drown in them is treating release flags as temporary by construction: the flag ships with the feature, and removing the flag (and the now-dead old code path) is a tracked follow-up task, not an optional cleanup that happens "eventually."
| Flag type | Purpose | Typical lifetime |
|---|---|---|
| Release flag | Gate a feature during rollout | Days to weeks — deleted once at 100% |
| Ops / kill switch | Emergency disable for a risky dependency | Long-lived, part of the system |
| Permission / entitlement | Gate a feature by plan or role | Long-lived, business logic |
| Experiment (A/B) | Compare two variants on a metric | Weeks — deleted when the experiment concludes |
🧠 Check your understanding
0/2 · 0/2 answered1. Why is deterministic hashing (user ID + flag key) preferred over Math.random() for percentage rollouts?
2. What is the main risk of never deleting release flags after a feature reaches 100% rollout?