What you'll learn
Quick Answer
A feature flag is a conditional check in your code — run the new path if the flag is on for this user, otherwise run the old one — that lets you deploy new code to production while keeping it invisible, then turn it on gradually or instantly without a new deployment. It separates two events most teams accidentally treat as one: shipping code to a server, and releasing a feature to users.
Deploy and Release Are Two Different Events
Most teams start out treating "deploy" and "release" as the same button-press, because without any mechanism to separate them, they genuinely are: merge to main, deploy, and the new behavior is immediately live for every user at once. A feature flag is the mechanism that splits those into two independently-controllable events. Deploy — get this code running on production infrastructure — can happen whenever CI is green, because the new code sits behind a flag defaulting to off and is completely inert. Release — make this behavior visible or active for users — can happen at an arbitrary later moment, decided by whoever actually owns that call, without requiring a new deployment at all, and reversible in seconds if it goes wrong.
The concrete benefit this unlocks is trunk-based development without long-lived feature branches. A half-finished feature can merge to main behind a flag that's off, instead of living on a separate branch for weeks accumulating merge conflicts with everyone else's work. "Not finished yet" and "not merged yet" stop being the same constraint, which is a bigger deal for a team's daily workflow than it sounds.
What Is Actually Inside a Flag
Strip a flag down to its real components and there are four: a flag key (a stable string identifier used in code, like new-checkout-flow), a default/fallback value (what your code should do if the flag system can't be reached at all — more important than it sounds, covered next), targeting rules (which users or segments get which value), and an evaluation context (the data about the current request that targeting rules are checked against — user ID, plan tier, country, an internal-employee marker, and so on).
The fallback-value question is a real design decision, not an afterthought. What should your application do if the flag service times out or is briefly unreachable? A well-built SDK caches the last-known rule set locally in memory and keeps evaluating flags from that cache during an outage, only ever falling back to a hardcoded default if it has never successfully fetched anything at all since startup. Treat "the flag service is down" as "keep using the last rules you had," not as "silently flip every flagged feature to its default at once" — the second behavior turns a flag-service blip into a self-inflicted outage of its own.
Rollout Strategies: From On/Off to Percentage Ramps
A handful of real patterns cover most use cases. A simple boolean toggle is often used more for operational config than for features — an environment-wide on/off switch. Targeted rollout shows a feature to a specific group first — the internal team, then a named beta customer list — matched by evaluating targeting rules against the request's attributes. Percentage rollout exposes a feature to a growing slice of the overall user base over time: 1%, then 5%, then 25%, then everyone. A kill switch is a flag whose entire purpose is turning something off instantly during an incident — far cheaper and faster than a full rollback and redeploy.
Percentage rollout only works if it's sticky: the same user has to get the same answer every single time they're evaluated, or they'll flicker between old and new behavior mid-session — confusing at best, and capable of corrupting in-progress state at worst (imagine a user starting a multi-step checkout on the new flow and getting bounced into the old flow's different data shape halfway through). The standard way to guarantee stickiness without a database lookup on every single request is deterministic hashing: hash a stable key like flag-name:user-id into a number, bucket that number into a fixed range (say 0-99), and compare the bucket against the rollout percentage. The same input always produces the same bucket, with no state to store anywhere.
Here is a real, working version of that logic in JavaScript — simplified from what production flag SDKs actually do, but the mechanism is the same one they use:
function fnv1a(str) {
let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
function isEnabled(flagKey, userId, rolloutPercent) {
const bucket = fnv1a(`${flagKey}:${userId}`) % 100;
return bucket < rolloutPercent;
}Running that against 20,000 synthetic user IDs at a 30% rollout, and separately checking the same user against the same flag five times in a row, produces this actual, captured output:
Enabled for 5947 of 20000 users (29.73%)
Stable across repeated calls: true [ false, false, false, false, false ]The gap between the requested 30% and the observed 29.73% — about a quarter of a percentage point on a run of 20,000 — is exactly the kind of sampling noise you'd expect from a reasonable hash function distributing a finite population, not a bug. And the same user, checked against the same flag five separate times, got the identical answer all five times — the stickiness the whole approach depends on.
Server-Side vs Client-Side Evaluation
Server-side flags are evaluated in backend code, which makes them safe to gate anything sensitive — pricing logic, unreleased API behavior, a security-relevant code path — because the rule set and the "off" variant's logic never have to reach the browser at all.
Client-side flags are evaluated in the browser or a mobile app, and they're necessary for purely visual decisions — show or hide a button, swap which component renders — where round-tripping to a server just to decide what to render would be far too slow. The caveat worth stating plainly: anything shipped to a client is inspectable by that client. A client-side flag is not a security boundary. A curious user can find the flag payload and whatever "hidden" variant it evaluates to for them sitting in their browser's network tab or bundled JS, in plain sight.
The common middle-ground architecture is an SDK that streams or polls rule updates from a flag service into a local, in-memory cache — LaunchDarkly's streaming connection and Unleash's polling model both work this way. Evaluation itself then becomes a fast local lookup on every check, not a network call per read; the network call only happens when the rules themselves change, which is far less frequent than every page render or every request.
The Cost Side: Flag Debt
Every flag is a branch, and both paths of that branch have to be maintained — and tested — for as long as the flag exists. A flag left at "100% on, forever" a year after its rollout finished isn't free just because nobody's actively working on it; it's dead conditional logic that every future reader of that function still has to mentally parse, and that every test suite touching that code path still has to account for.
The problem compounds with more than one live flag at a time. Two simultaneous flags create up to four meaningful combinations of behavior actually running in production; five flags create up to thirty-two. Almost none of those combinations are being deliberately tested end to end, which is a common, concrete cause of "it worked fine in staging" surprises — staging frequently isn't running the same combination of flag states that production happens to be running at that exact moment.
The fix isn't avoiding flags — it's treating a rollout as genuinely incomplete until the flag itself is deleted and the old code path removed, not just until it reaches 100%. Some teams enforce this with an explicit expiry date attached to each flag at creation time, or an automated "stale flag" report that nags whichever engineer owns a flag that's been sitting at a fixed value (100% or 0%) for longer than some agreed threshold, treating an un-cleaned-up flag as its own small piece of technical debt with an owner.
Build vs. Buy, and the OpenFeature Standard
The real landscape splits roughly three ways. Hosted vendors — LaunchDarkly, Split, Statsig — offer full targeting UIs, audit logs, and experimentation analysis built directly into the product. Open-source, self-hosted options — Unleash, Flagsmith, GO Feature Flag — give teams the same underlying pattern without a per-seat bill, at the cost of running and maintaining the service yourself. And for a small team's very first flag, a plain database table plus a small evaluation function is a genuinely reasonable starting point — the pattern (deploy/release separation, sticky rollout logic) matters far more than the tooling at small scale.
Worth knowing about specifically: OpenFeature is a real, vendor-neutral API specification — a Cloud Native Computing Foundation project that became a CNCF incubating project in December 2023 — with SDKs across most major languages. Application code calls a single, standard interface, and the actual flag provider underneath it (LaunchDarkly, Flagsmith, Unleash, or a custom in-house provider) can be swapped without touching every call site scattered across a large codebase. It exists specifically to address the realistic worry of getting locked into one vendor's particular SDK shape after that shape has spread through hundreds of files.
