Shipping code and releasing a feature are different events
Feature flags let a team deploy code continuously while controlling *who sees what feature and when* as a completely separate decision — no redeploy needed to turn something on, roll it back, or ramp it from 1% to 100% of users. Whether you use LaunchDarkly, Unleash, Flagsmith, or a homegrown system, the mechanism is the same: your app fetches a JSON payload describing every flag and its rules, evaluates it locally against the current user, and gets back a value.
A single flag's JSON shape
{
"key": "new-checkout-flow",
"enabled": true,
"defaultVariant": "control",
"variants": [
{ "key": "control", "value": false, "weight": 50 },
{ "key": "treatment", "value": true, "weight": 50 }
],
"targets": [
{ "userId": "usr_9182", "variant": "treatment" }
]
}enabled is the master kill switch — if false, the flag always evaluates to defaultVariant regardless of everything else. targets lets you force a specific user into a specific variant (useful for an internal team dogfooding a feature before wider rollout), overriding the percentage-based variants split for everyone else.
Percentage rollouts: deterministic, not random
A 50/50 rollout can't literally flip a coin on every request — the same user must consistently land in the same variant on every page load, or the UI would flicker between two experiences. The standard technique is consistent hashing: hash the user ID plus the flag key into a number, then map that number into a bucket:
function evaluateVariant(flag, userId) {
if (!flag.enabled) return flag.defaultVariant;
const override = flag.targets.find((t) => t.userId === userId);
if (override) return override.variant;
const hash = hashToPercent(flag.key + ":" + userId); // deterministic, 0-99
let cumulative = 0;
for (const variant of flag.variants) {
cumulative += variant.weight;
if (hash < cumulative) return variant.key;
}
return flag.defaultVariant;
}The same userId and flag.key combination always hashes to the same bucket, so a user's assignment is stable across sessions — but a *different* flag on the same user hashes independently, so rollouts don't correlate with each other unless explicitly designed to.
Targeting rules: conditions beyond percentage
Real flag systems layer conditional rules before falling back to a percentage split — "100% on for internal staff, 10% for everyone else":
{
"key": "new-checkout-flow",
"rules": [
{ "condition": { "attribute": "email", "operator": "endsWith", "value": "@ourcompany.com" }, "variant": "treatment" },
{ "condition": { "attribute": "plan", "operator": "equals", "value": "enterprise" }, "variant": "control" }
],
"variants": [
{ "key": "control", "value": false, "weight": 90 },
{ "key": "treatment", "value": true, "weight": 10 }
]
}Rules are evaluated in order, top to bottom, with the first matching condition winning — the percentage variants split only applies to users who fall through *every* explicit rule unmatched, which is exactly why rule order matters and is usually significant in a flag platform's UI.
The client SDK payload: bulk-evaluated flags
Rather than one request per flag, most SDKs fetch every flag relevant to a user in one payload on app load, then evaluate all of them client-side without further network calls:
{
"userId": "usr_9182",
"flags": {
"new-checkout-flow": { "value": true, "variant": "treatment" },
"dark-mode-default": { "value": false, "variant": "control" },
"beta-search": { "value": true, "variant": "treatment" }
}
}This is a pre-evaluated response — the server (or SDK, for local evaluation) has already run the targeting logic and just hands the client the final answer per flag, so the app doesn't need to know or care about rules and percentages at all; it just reads flags["new-checkout-flow"].value.
Local vs. server-side evaluation
Two architectures exist, and the JSON differs accordingly: server-side/remote evaluation sends the user's attributes to the flag service and gets back only the resolved values (as above); client-side/local evaluation downloads the *entire rule set* (including all the targeting logic and percentages from the earlier examples) once, then evaluates flags locally on every check with zero added latency — a common choice for high-frequency flag checks inside a hot code path, at the cost of exposing your targeting rules to anyone who inspects the downloaded payload.