Feature flags decouple deploying code from releasing a feature
“Deploy” and “release” get used as synonyms, and separating them is one of the highest- leverage moves in a frontend team’s workflow. Deploying is shipping code to production. Releasing is turning a feature on for users. A feature flag is the switch that decouples them: you can merge and deploy unfinished code that stays dark — present but off — until you flip the flag. Once deploy no longer means release, a cascade of good things becomes possible: developers merge small increments to main without waiting for a whole feature, releases become a config change instead of a deploy, and a bad rollout is a flag flip away from being undone.
The flag is a runtime switch, not a build-time one
A feature flag is checked at runtime, so the same deployed build behaves differently depending on the flag’s value — which is what lets you change behaviour without shipping new code:
// the code is deployed either way; the flag decides what users actually get
function Checkout() {
return useFlag("new-checkout") // read at runtime, from a config service
? <NewCheckout /> // dark until the flag is flipped on
: <LegacyCheckout />;
}
Merge NewCheckout half-finished behind new-checkout: false, and it sits safely in
production, invisible, until it is ready — no long-lived branch, no big-bang merge.
Flags enable gradual, reversible rollouts
Because the flag is config, releasing can be gradual and targeted: on for internal users first, then 1% of traffic, then 50%, then everyone — watching metrics at each step and flipping back instantly if something breaks:
// release to a slice, not the world — and reverse it in seconds if metrics dip
function useFlag(name, userId) {
const rule = config[name]; // { enabled: true, rollout: 0.1 }
return rule.enabled && hash(userId) % 100 < rule.rollout * 100; // 10% of users
}
A rollback that used to mean an emergency deploy is now a toggle, which is a categorically faster and safer incident response.
The discipline: flags are debt, so retire them
The catch is that flags accumulate. Every one is a branch in your code and a
combination to reason about; leave them forever and you get an untestable thicket of
if (flagA && !flagB). So treat flags as temporary by default — a release flag exists
to ship a feature, and once the feature is fully rolled out and stable, the flag and
the dead branch get deleted. (Long-lived flags for genuine configuration —
kill-switches, plan tiers — are a separate, deliberate category.) Managed well, feature
flags turn deploy and release into two independent, low-risk actions: merge small and
often, release when ready, roll back in seconds, and clean up the switch afterward. The
render-strategy-choice exercise touches the same “decide behaviour at the boundary”
thinking a flag formalises — choosing what users get, separately from what you shipped.