A22 · Part 3 · Advanced
Feature flags
Deploying code and releasing it are different decisions, and a flag is what separates them.
06On this page
What a flag is for
if (flags.newCheckout) return <NewCheckout />;
return <OldCheckout />;
The code for the new checkout is merged, built and deployed — but it is off. Turning it on is a configuration change, not a deploy, and turning it off again takes seconds instead of a rollback. That separation is the whole value: engineers merge to the main branch daily without waiting for a feature to be finished, and product decides when users see it.
| Kind | Lifetime | Example |
|---|---|---|
| Release flag | days to weeks, then deleted | the new checkout, rolled out to 10%, then 100% |
| Kill switch | permanent | turn off the recommendations service when it is down |
| Experiment | until the result is in | two pricing layouts, measured |
| Entitlement | permanent | the export feature, for the paid plan only |
The kinds behave differently, and confusing them is the source of most flag pain. A release flag that lives for a year is a permanent fork in the code. A kill switch that gets removed as "clean-up" removes your emergency exit.
Evaluation needs context
const enabled = await client.getBooleanValue("new-checkout", false, {
targetingKey: user.id,
plan: user.plan,
country: user.country,
});
A flag is not a boolean; it is a function of who is asking. The
evaluation context — user id, plan, country, app version —
feeds rules such as "on for staff", "on for Germany", or "on for 10% of users".
The last argument, false, is the default returned if the
flag service is unreachable or the flag is missing. Make it the old
behaviour, always; a default that turns the new feature on means an outage in
your flag provider ships an unfinished feature to everyone.
Percentage rollouts must be sticky
function bucket(flagKey, userId) {
let h = 2166136261; // FNV-1a hash
const text = flagKey + ":" + userId;
for (let i = 0; i < text.length; i++) {
h ^= text.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return (h >>> 0) % 100; // 0 to 99, the same every time
}
const enabled = bucket("new-checkout", user.id) < rolloutPercent;
Rolling out to 10% by calling Math.random() would put the same
user in and out of the feature on every page load. Hashing the flag key and
user id gives each user a fixed position from 0 to 99, so a user is either in
the ten or not, and raising the rollout to 25% only adds people.
Including the flag key in the hash means each flag picks a different ten
percent, instead of the same unlucky users testing everything.
Where the flag is evaluated
// Flags SDK (Vercel), server-side
import { flag } from "flags/next";
export const newCheckout = flag({
key: "new-checkout",
decide() { return false; },
});
export default async function Page() {
const enabled = await newCheckout();
return enabled ? <NewCheckout /> : <OldCheckout />;
}
Evaluate on the server where you can. A client-side flag has to be fetched after the page loads, so users first see the old UI and then watch it change — a layout shift, and a flicker that ruins any experiment measuring the first impression. The Flags SDK evaluates on the server only for this reason, and offers a precompute pattern so flagged pages can still be statically generated instead of falling back to per-request rendering.
Caching is the catch. A page that varies by flag is a page that varies by user, and a CDN happily serves one user's variant to everyone unless the variant is part of the cache key. Flags and caching have to be designed together.
OpenFeature: a standard, not a vendor
import { OpenFeature, OpenFeatureProvider, useBooleanFlagValue } from "@openfeature/react-sdk";
OpenFeature.setProvider(myProvider); // LaunchDarkly, Unleash, Flagsmith, in-memory…
<OpenFeatureProvider><App /></OpenFeatureProvider>
function Checkout() {
const enabled = useBooleanFlagValue("new-checkout", false);
return enabled ? <NewCheckout /> : <OldCheckout />;
}
OpenFeature is a vendor-neutral API for evaluating flags. Your components call
useBooleanFlagValue; a provider plugs in whichever service
you use, so changing vendors means changing one line, not every call site. The
hooks re-render when the evaluation context or the flag configuration changes,
and can suspend until the provider is ready. The Flags SDK has an OpenFeature
adapter, so the two combine: the framework integration and precompute from one,
provider freedom from the other.
The costs
- Flag debt. Every flag doubles the paths through the code and the states a bug can hide in. Give each release flag an owner and a removal date when you create it, and treat "delete the flag" as part of finishing the feature.
- Combinatorial testing. Ten independent flags is a thousand combinations. Test the default state and the flag you are changing, and keep long-lived flags few.
- Client-visible means public. A flag sent to the browser can be read in the network tab, including the name of a feature you have not announced. Put nothing sensitive in flag names or payloads.
- A flag is not authorisation. Hiding the export button for free users is UX; the export endpoint must still check the plan. The same rule as route guards.
- Experiments need more than a flag. You also need consistent assignment, logging that a user actually saw the variant, and enough traffic to conclude anything. A flag on its own is only the switch.
A rollout, step by step
- Merge the feature behind a flag that is off. Deploy freely.
- Turn it on for the team, then a small percentage of real users.
- Watch error rate and the relevant business metric between each step; increase to 25%, 50%, 100%.
- If a metric moves the wrong way, turn the flag off. No deploy, no rollback.
- When it has been at 100% for a while, delete the flag and the old code.
Interview answer, one sentence
"A flag separates deploy from release: code ships dark and is turned on by configuration, gradually and reversibly, using a sticky hash of the user id so people stay in or out; I evaluate it on the server to avoid flicker, default to the old behaviour if the service is down, never use a flag as authorisation, and delete each release flag once it is at a hundred percent."
Opens in the editor: write it, run it, and check it against real tests.
All 54 for this level →Finished reading?
0 of 57 chapters read in this series