Feature flags are debt, not configuration
Every flag doubles the reachable code paths and no flag ever gets deleted by itself. Giving each one a removal date matters far more than building a clever flag system.
The problem feature flags solve is clear: decouple deploy from release so you can canary and roll back. The problem they create is just as clear: every flag doubles the possible state combinations, and nobody owns deleting it.
Three kinds, three lifetimes
| Kind | Lifetime | Example |
|---|---|---|
| Release flag | days to weeks | canary the new homepage |
| Ops flag | months, maybe forever | kill switch for a dependency |
| Experiment flag | one experiment | A/B test |
Managing all three in one system is where the mess starts. Release flags must be deleted. They are scaffolding for the rollout, nothing more.
Use a date, not a boolean
An expiry field on the flag beats any written convention:
const FLAGS = {
newCheckout: { enabled: false, expiresAt: '2026-10-31' },
legacySearch: { enabled: true, expiresAt: '2026-12-01' },
} as const;
Pair it with one test: an expired flag fails the build.
for (const [name, f] of Object.entries(FLAGS)) {
if (Date.parse(f.expiresAt) < Date.now()) {
throw new Error('flag ' + name + ' is past its removal date');
}
}
That assertion turns “clean up later” into “deal with it now”. Without it, flags live in the code forever.
Reads need a default
function isEnabled(name: keyof typeof FLAGS): boolean {
const override = process.env['FLAG_' + name];
return override ? override === '1' : FLAGS[name].enabled;
}
Keep the default in code and let the environment variable be a temporary override. The reverse (default off, config turns it on) means a new environment silently ships with the feature disabled.
Keep the branch in one place
A flag should appear in exactly one spot. Scattered if (flag) checks make removal nearly impossible.
// good: one place chooses the implementation
export const checkout = isEnabled('newCheckout') ? newCheckout : legacyCheckout;
// bad: branches all over the implementation
export function checkout(cart) {
if (isEnabled('newCheckout')) { /* ... */ }
if (isEnabled('newCheckout')) { /* ... */ }
}
The first is deleted by removing a line and a file. The second needs a merge undone in dozens of places.
A flag is not a business rule
If a flag has been on for three months with no plan to turn it off, it is not a flag any more. It is configuration and belongs in the real code path. Calling it a flag just makes people think it can be dropped at will.
A flag earns its keep during the rollout. Every day it survives afterward, it makes the code harder to read and test.

Comments
…