A feature flag is an if statement whose condition lives outside the code. That small idea changes how teams ship. You can merge a half-built checkout redesign into main on Tuesday, deploy it switched off, turn it on for staff on Thursday, then for 10% of customers, and switch it off in seconds if payment errors climb. No branch, no redeploy, no rollback of unrelated changes.
The interview angle is rarely “what is a feature flag?” It’s what goes wrong. Flags that never get removed, rollouts that flip users back and forth, tests that only cover one combination, and a configuration change that took production down with no code deploy at all. Those are what the interviewer wants to hear you talk about.
Deploy and release become separate events
Without flags, deploying code and exposing it to users are the same moment. With flags, a deploy puts the code on servers in a dormant state, and a release is a configuration change that turns it on for some audience. That separation is what makes trunk-based development workable: unfinished work merges daily behind a flag that’s off, so there are no long-lived feature branches drifting from main.
It also changes what “rollback” means. If the new checkout misbehaves, you turn the flag off, which takes effect as soon as servers see the new configuration, usually seconds. Redeploying the previous build would also revert every other change that went out in the same deploy.
Four kinds of flag, with different lifetimes
Treating all flags the same is the root of most flag debt. Pete Hodgson’s widely cited categories help:
- Release flags hide incomplete or unreleased features. They should live days or weeks and then be deleted along with the old code path.
- Experiment flags split users between variants for an A/B test. They live as long as the experiment needs a statistically meaningful result, then get removed.
- Ops flags (kill switches) let operators turn off an expensive or risky feature under load, like recommendations on a product page during a sale. These can be permanent, and they need to be tested regularly or they won’t work when needed.
- Permission flags turn features on for particular customers or plans. They’re really product configuration and often belong in an entitlements system rather than a flag tool.
When an interviewer asks “how long should a flag live?”, the good answer is “it depends on its type, and you decide when you create it.”
Rolling out to a percentage without flipping users
“Enable for 10% of users” sounds simple. The common mistake is implementing it as Math.random() < 0.1 on each request. A user then sees the new checkout, refreshes and sees the old one, and their cart behaves differently between the two. Metrics comparing the groups become meaningless because nobody is consistently in either group.
The fix is to hash a stable identifier together with the flag name and map it to a bucket from 0 to 99:
import { createHash } from 'node:crypto';
// Same user + same flag -> same bucket, on every server, every request.
function bucket(flagKey, userId) {
const hash = createHash('sha256').update(`${flagKey}:${userId}`).digest();
return hash.readUInt32BE(0) % 100; // 0..99
}
export function isEnabled(flag, userId) {
if (!flag.enabled) return false;
return bucket(flag.key, userId) < flag.rolloutPercent;
}Running this over 10,000 synthetic user IDs with Node 22:
10% rollout: 1020 of 10000 users enabled
second evaluation: 1020 enabled, 0 users changed
10% -> 25%: 2466 enabled, 0 early users dropped
Math.random() per request: 1837 users would see a different answer on reloadThe hashed version gives roughly 10%, gives every user the same answer each time, and when you widen to 25% the original 10% stay in, because their bucket numbers are still below the threshold. The per-request random version sends around 18% of users a different answer on the next request (the exact number changes each run). Including the flag key in the hash matters too: without it, the same unlucky 10% of users would be first in line for every experiment.
For logged-out visitors, use a long-lived anonymous ID in a cookie. For features that affect a whole account, such as a billing change, bucket by account ID, so colleagues sharing an account see the same thing.
Who owns the flag, and when does it die?
The cost of a flag isn’t the if. It’s that each live flag doubles the number of code paths that could be running in production. Ten long-forgotten release flags mean 1,024 possible combinations, most never tested. Old flags also make code hard to read, and a flag that’s been fully on for a year still has a dead else branch that someone will eventually “fix.”
So every flag should be created with metadata: a key, a type, an owner (a team, not a person who might leave) and, for release and experiment flags, an expiry date. Keep that registry in the repository, where a CI step can read it:
[
{ "key": "new-checkout", "type": "release", "owner": "payments-team", "expires": "2026-11-15" },
{ "key": "pdf-invoices", "type": "release", "owner": "billing-team", "expires": "2026-08-01" },
{ "key": "disable-recommendations", "type": "ops", "owner": "platform-team", "expires": null }
]A short script fails the build when a release flag has no expiry or has passed it, and uses git grep to show where the flag is still referenced:
for (const flag of flags) {
if (!flag.owner) { console.error(`${flag.key}: no owner`); failed = true; }
if (flag.type === 'release' && !flag.expires) {
console.error(`${flag.key}: release flags need an expiry date`); failed = true;
}
if (flag.expires && flag.expires < today) {
let refs = '';
try { refs = execFileSync('git', ['grep', '-n', `'${flag.key}'`, '--', 'src'], { encoding: 'utf8' }); } catch {}
console.error(`${flag.key}: expired ${flag.expires}, owner ${flag.owner}`);
if (refs) console.error(refs.trimEnd().replace(/^/gm, ' '));
failed = true;
}
}Run on 7 October 2026 in a test repository:
pdf-invoices: expired 2026-08-01, owner billing-team
src/invoice.js:2:export const invoiceFormat = (user) => (isEnabled('pdf-invoices', user) ? 'pdf' : 'html');Failing every build on a date can be too blunt for a busy team. Many make it a warning first and a failure two weeks later, or open a ticket for the owner automatically. The point is that removal is scheduled when the flag is born, rather than left to someone’s good intentions.
Removing a flag is a code change: delete the check, delete the losing branch, delete the flag from the registry and the flag service. Do it in that order. Deleting the flag from the service first makes the SDK return its default, which may be “off,” turning a fully released feature off for everyone.
Testing with flags in the code
You can’t test every combination, but you can test the ones that ship. At minimum, run the suite with each release flag in its current production state and in the state it’s moving to. For a flag that’s about to go to 100%, that’s on and off. Kill switches deserve a regular drill, because an ops flag that hasn’t been flipped in a year may now break the page it was meant to protect.
Also remember that flag changes are production changes. Who can flip a flag, whether it needs approval, and whether flips are logged next to deploys in your incident timeline all matter. “Nothing was deployed” is a misleading thing to say during an incident if someone changed a flag.
Interview practice
Your company has about 300 feature flags. Engineers say nobody knows which are safe to remove, an incident last month was caused by an old flag being switched off by mistake, and new A/B tests keep landing on the same small group of users. What would you change?
Answer
The three symptoms have separate causes. Not knowing which flags are removable means flags have no type, owner or expiry. The accidental switch-off means old release flags were never cleaned up, so a fully launched feature still depended on a toggle. And experiments hitting the same users suggests bucketing hashes the user ID alone, without the flag key, so a user in the bottom 10% of one experiment is in the bottom 10% of all of them.
First, inventory. For each flag, find its references with git grep, its current state in the flag service and when it last changed. Flags that have been 100% on or off for months, with no recent changes, are candidates for removal. Assign each remaining flag a type and an owning team. Second, put that metadata in a registry in the repository and add a CI check that requires an owner for every flag and an expiry for release and experiment flags, with warnings before failures. Third, remove the stale release flags in small pull requests, deleting the losing branch and only then the flag itself. Fourth, change bucketing to hash the flag key together with the user ID, so each experiment draws an independent sample. Finally, restrict who can change production flags and log every change alongside deploys, so the next incident timeline shows flag flips.