A feature flag client decides whether a feature is turned on for a given user, combining a master on/off switch, targeting rules that force specific users on or off, and a percentage rollout that ramps a feature to a growing slice of the audience. Teams ship code behind flags so they can release to 1% of users, watch for errors, then widen to 100% with no redeploy. The catch is that a rollout has to be sticky: a user in the enabled 10% today must stay in it tomorrow, or you can never measure the effect of a change. You will build the evaluator at the center of this — a factory that reads a flag config and answers two questions for any user.
type Flag = {
enabled: boolean; // master switch — false means off for everyone
rollout?: number; // 0..100; the share of users the flag is on for
rules?: Array<{ // targeting overrides, checked in order
attribute: string;
value: unknown;
enabled: boolean;
}>;
variants?: Array<{ key: string; weight: number }>; // A/B buckets
};
type Context = { userId: string; [attribute: string]: unknown };
function featureFlagClient(flags: Record<string, Flag>): {
isEnabled(key: string, context: Context): boolean;
variant(key: string, context: Context): string | null;
};
A 50% rollout that stays sticky per user:
const client = featureFlagClient({
'checkout-v2': {
enabled: true,
rollout: 50,
rules: [{ attribute: 'plan', value: 'enterprise', enabled: true }],
variants: [
{ key: 'control', weight: 60 },
{ key: 'treatment', weight: 30 },
{ key: 'holdout', weight: 10 },
],
},
});
// u_530 falls in the enabled half (bucket 22 < 50) and stays there on every call.
client.isEnabled('checkout-v2', { userId: 'u_530' }); // true
client.isEnabled('checkout-v2', { userId: 'u_530' }); // true — same answer, always
// u_842 falls in the disabled half (bucket 80 >= 50).
client.isEnabled('checkout-v2', { userId: 'u_842' }); // false
A targeting rule overriding the rollout, and a sticky variant:
// u_842 is off by the rollout, but the enterprise rule forces it on —
// rules are checked before the rollout, and the first match wins.
client.isEnabled('checkout-v2', { userId: 'u_842', plan: 'enterprise' }); // true
// An enabled user draws a sticky variant; a user who is not enabled gets null.
client.variant('checkout-v2', { userId: 'u_530' }); // 'treatment'
client.variant('checkout-v2', { userId: 'u_842' }); // null — not in the experiment
enabled is false, isEnabled returns false, and no rule or rollout can turn it back on.rules in order; the first rule whose context[attribute] equals its value returns that rule's enabled, ignoring the rollout entirely.userId, never from Math.random, so the same user always lands in the same bucket. A rollout of 100 is on for everyone, 0 for no one; a missing rollout means fully on.null when the flag is not enabled for that user.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.