system-design

Design a Feature Flag Service (LaunchDarkly)

Difficulty: Tier 2 Asked at: Amazon, LaunchDarkly, most product companies Time budget: 45 min

Feature flags let you turn features on/off and roll them out to a percentage of users without deploying — the backbone of safe releases, A/B tests, and kill switches. The system is deceptively simple but has a sharp constraint: flag evaluation happens on the hot path of every request, so it must add near-zero latency, which drives a local-evaluation + config-push design. It ties together config distribution, caching, and deployment safety.

Prerequisites: Caching, Deployment & Infra, Real-Time Communication


1. Requirements

Functional:

Non-functional:

Out of scope: the experimentation stats engine, the dashboard UI internals.


2. Estimation


3. The core insight: evaluate locally, push config

🚨 Don’t call the flag service on each evaluation. Instead, push flag definitions to each app server (via an SDK) and evaluate locally in-process.

flowchart TB
    Dash[Flag config dashboard] --> FlagSvc[Flag Service]
    FlagSvc --> Store[(Flag config store)]
    FlagSvc -->|push updates<br/>stream / poll| SDK1[SDK in App Server 1<br/>local flag cache]
    FlagSvc --> SDK2[SDK in App Server 2<br/>local flag cache]
    App1[Request] -->|evaluate locally, ~0ms| SDK1

4. Deep dives

4a. Percentage rollouts & consistent bucketing

“Enable for 5% of users” must be deterministic and sticky — the same user always gets the same result (no flickering), and 5% means a stable 5%. Achieve it by hashing (user_id + flag_key) → a number in [0,100) and comparing to the rollout percentage. 🚨 Same hash → same bucket every time, consistent across all servers (they share the rule), and increasing the percentage only adds users. This deterministic hashing is the key trick for rollouts and A/B bucketing.

4b. Fast propagation & kill switches

A kill switch (turn off a broken feature now) demands seconds-level propagation. Use a push channel (streaming) so SDKs get updates immediately rather than waiting for a poll interval. Trade-off: push is more complex but far faster than polling; many systems do streaming with polling as a fallback. (Real-Time Communication)

4c. Availability — fail safe

🚨 The flag service must never take down the app. If an SDK can’t reach the service:

4d. Targeting rules evaluation

Rules can target by user attributes (country, plan, beta group), segments, and percentages, combined with AND/OR. The SDK evaluates these locally against the request context. Keep rules simple enough to evaluate in microseconds. Order rules (specific overrides before percentage rollout).

4e. A/B experiments

An experiment = a multivariate flag with consistent per-user bucketing (the hashing trick), plus logging which variant each user saw (for the stats engine to correlate with outcomes). Ensure a user stays in the same variant for the experiment’s duration (sticky bucketing).

4f. Scale & consistency

Config is tiny and read-mostly; the service fans out updates to many SDKs. Eventual consistency of propagation is fine (a few seconds’ skew between servers during a change is acceptable — briefly some requests see old, some new). For changes that must be atomic across the fleet, that brief skew is the trade-off; usually acceptable.


5. Bottlenecks & scaling further

  1. Evaluation latency → local in-SDK evaluation, zero network calls per request.
  2. Propagation speed → streaming push to SDKs (+ polling fallback).
  3. Availability → SDKs cache last-known config; safe defaults; never block the app.
  4. Fan-out to many SDKs → the service streams small config diffs to all connected SDKs.
  5. Consistent rollouts → deterministic hashing of (user, flag).

6. Trade-off summary

Decision Chosen Alternative Why
Evaluation Local in-process (SDK) Remote call per check ~0 latency on the request hot path
Propagation Streaming push Polling only Kill switches need seconds, not minutes
Rollout bucketing Deterministic hash (user+flag) Random per request Sticky, consistent, monotonic rollout
Availability Cache last-known + safe defaults Depend on the service Flag outage must not break the app
Consistency Eventual (seconds) across fleet Atomic fleet-wide Brief skew acceptable; simpler, available

7. Follow-up questions

Why not just call the flag service to evaluate each flag? Because flag checks happen on the hot path of nearly every request — often many per request — so a network call per evaluation would add latency and a dependency to essentially all of your traffic, which is unacceptable both for performance (milliseconds added everywhere, multiplied by many checks) and for reliability (the flag service becomes a hard dependency that can slow or break every request). Instead, the flag *definitions* — which are tiny and change infrequently — are pushed to an SDK embedded in each app server, which holds them in memory and evaluates flags locally in microseconds with no network call. Freshness comes from the service pushing config updates to the SDKs (via streaming, with polling as a fallback), so changes still propagate in seconds. This inverts the naive design: rather than sending each request's context to the flags, you send the (small, slow-changing) flags to where the requests are, getting near-zero evaluation latency and removing the flag service from the per-request critical path. Local evaluation plus config push is the core architectural decision.
How do you roll a feature out to exactly 5% of users, consistently? By hashing a stable identifier for the user together with the flag key into a number in a fixed range (say 0–100) and enabling the feature when that number falls below the rollout percentage. Because the hash is deterministic, a given user always maps to the same bucket for that flag, so they consistently get the same result on every request and across every server (all of which share the same rule and hashing) — no flickering between on and off. Hashing the user *with the flag key* means a user's bucket differs per flag, so you're not always exposing the same 5% of people to every rollout. And because you compare against a threshold, raising the rollout from 5% to 10% only *adds* users (those whose hash is between 5 and 10) without churning the original 5% — a monotonic, sticky rollout. The same mechanism gives A/B experiments consistent per-user variant assignment. Deterministic hashing of (user, flag) is the trick that makes percentage rollouts stable, distributed-consistent, and monotonic.
The flag service goes down. What happens to the app? Nothing bad, by design — a flag-service outage must degrade to "flags stop changing," never "the app breaks." Each SDK holds the last-known flag configuration in local memory, so if it can't reach the service it simply keeps evaluating flags against that cached config, and the application continues serving traffic normally with whatever flag states were last known. New instances starting up during an outage bootstrap from a cached or bundled local config file so they aren't flagless, and if truly no config is available, flags evaluate to safe defaults (typically "feature off"), which is the conservative choice. The whole flag path is engineered to be non-critical: it's a read-mostly, cached, locally-evaluated system precisely so that the availability of the central service never gates request handling. This fail-safe posture is essential because feature flags sit in the path of nearly every request, so they must be more available than the service that manages them.
How fast must a kill switch propagate, and how do you achieve it? A kill switch needs to propagate in seconds, because its whole purpose is to instantly disable a feature that's causing problems (errors, an incident) without waiting for a deploy — minutes of delay could mean minutes of outage. You achieve near-instant propagation by pushing config changes to the SDKs over a streaming channel (server-sent events or a websocket) so that the moment an operator flips the flag, the update is pushed to all connected app servers and takes effect on the next request, rather than waiting for the SDKs to poll on some interval. Polling can serve as a fallback (and a safety net if a push is missed), but push is what delivers the seconds-level latency a kill switch requires. The trade-off is that maintaining streaming connections to a large fleet is more complex than periodic polling, but the fast propagation is worth it for the safety-critical case, and it's why production flag systems favor streaming for updates.

8. What junior / mid / senior answers look like


Further reading