Back to System Design
System Design
easy
mid

How would you implement a reusable feature toggle system with multiple conditions?

A flag evaluator: flags defined with rules (boolean, user/role targeting, percentage rollout, environment, date windows), evaluated against a context (user, env). Expose via a provider + useFlag hook. Cache the evaluated flags, support remote config, and default safely when evaluation fails.

5 min read·~12 min to think through

A feature toggle system is a rules engine: given a flag's conditions and a context, decide on or off — wrapped in an ergonomic API.

The flag model

A flag isn't just a boolean — it has rules:

js
{
  key: "new-checkout",
  enabled: true,                          // master switch
  rules: [
    { type: "user",        userIds: ["u1", "u2"] },     // explicit targeting
    { type: "role",        roles: ["beta-tester"] },
    { type: "percentage",  rollout: 25 },               // gradual rollout
    { type: "environment", envs: ["staging"] },
    { type: "dateWindow",  start: "...", end: "..." },
  ],
}

The evaluator

A pure function: (flag, context) => boolean, where context is { userId, role, env, ... }.

js
function evaluate(flag, context) {
  if (!flag.enabled) return false;
  // a flag is ON if the context matches its rules (any/all per design)
  return flag.rules.some((rule) => matchesRule(rule, context));
}
  • Percentage rollout must be stable per user — hash userId + flagKey to a number 0–99 and compare to rollout. Same user always gets the same answer; otherwise the UI flickers on every load.
  • Decide any vs all rule semantics (OR vs AND), or support both.

The React API

jsx
<FeatureFlagProvider flags={flags} context={{ userId, role, env }}>
  ...
</FeatureFlagProvider>

function Checkout() {
  const isNew = useFlag("new-checkout");
  return isNew ? <NewCheckout /> : <OldCheckout />;
}

A provider holds the flags + context and the evaluated results; useFlag(key) reads the evaluated value.

Production concerns

  • Remote config — flags fetched from a service (LaunchDarkly, or your own), so you flip features without a deploy. Refresh/poll or stream updates.
  • Caching & performance — evaluate once per flag per context, memoize; don't re-evaluate every render.
  • Safe defaults — if the flag service is unreachable or a flag is unknown, fall back to a safe default (usually "off") — never crash because flags failed to load.
  • SSR consistency — evaluate the same way on server and client to avoid hydration mismatch.
  • Cleanup discipline — flags are tech debt; track and remove stale ones.
  • Analytics/exposure logging — record which users saw which variant (for A/B analysis).
  • Kill switch — every risky flag can be turned off instantly.

The framing

"It's a rules engine. A flag has a master switch plus rules — explicit user/role targeting, percentage rollout, environment, date windows. A pure evaluator (flag, context) => boolean checks the context against the rules; the key subtlety is percentage rollout must hash userId + flagKey so it's stable per user, not random per load. I'd expose it as a provider holding flags and context, with a useFlag(key) hook reading memoized evaluations. Production-wise: remote config so I can flip flags without a deploy, safe 'off' defaults if the service is down, SSR-consistent evaluation, exposure logging for A/B, and discipline around removing stale flags."

Follow-up questions

  • Why must percentage rollout be stable per user?
  • What should happen if the flag service is unreachable?
  • How do remote flags let you change features without deploying?
  • How do you avoid feature flags becoming permanent tech debt?

Common mistakes

  • Random percentage rollout that flickers per page load.
  • No safe default when flag evaluation fails — crashing instead.
  • Re-evaluating flags on every render instead of memoizing.
  • Inconsistent server/client evaluation causing hydration mismatch.
  • Never removing stale flags — accumulating tech debt.

Performance considerations

  • Evaluate once per flag+context and memoize; don't block first render on a remote fetch — render with cached/default values and update when flags arrive. Streaming/polling updates should be debounced.

Edge cases

  • Flag service down or slow on load.
  • Unknown flag key requested.
  • User with no id (anonymous) for percentage rollout.
  • Conflicting rules within one flag.
  • Flag changes mid-session.

Real-world examples

  • LaunchDarkly, Split, Unleash — exactly this rules-engine + SDK model.
  • Gradual rollouts: shipping a feature to 5% → 25% → 100% via a percentage rule.

Senior engineer discussion

Seniors model flags as rule sets with a pure evaluator, make percentage rollout stable via hashing, expose a provider + hook, and cover remote config, safe defaults, SSR consistency, exposure logging, and flag-cleanup discipline.

Related questions