Skip to the solution
MediumSystem Design13 min read

How would you design a feature flag system (LaunchDarkly-style) at scale?

82 views

The problem

Question presented to candidate: "We want to turn features on and off for millions of users without redeploying, and use it as a kill switch during incidents. Design a feature flag service like LaunchDarkly that thousands of services and apps can rely on."

What a strong answer should cover:

  • Splitting a control plane (dashboard, API, flag store, audit log) from a data plane (SDKs that hold the rules in memory and evaluate locally, with no network call per evaluation)
  • Delivery of changes: a streaming connection (server-sent events) that pushes deltas in well under a second, with polling as a fallback, and a full re-sync after a reconnect
  • Per-flag versions so that late or duplicated updates never roll a flag back, and an append-only audit log of who changed what
  • Failure behaviour: the flag service is never on the request path; a running SDK keeps its last known values, a cold start falls back to code defaults or a persistent cache or relay
  • Scale numbers: evaluations per second versus connections and change fan-out, the bootstrap storm when a whole fleet restarts, and client-side SDKs that receive evaluated values instead of rules

Clarifying questions expected:

  • Server-side services only, or browsers and mobile apps too (which cannot be trusted with the rules)?
  • How fast must a kill switch take effect, and what should happen if the flag service is down?
  • Do we need experiments and analytics on flag evaluations, or only on/off and rollouts?

Code / implementation expected: Optional โ€” the example models capacity, streaming versus polling lag, versioned updates and outage fallbacks.

  • feature-flags
  • platform
  • experimentation

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

13 min read ยท 16 sections

Read the code

Flag delivery at scale: local vs remote evaluation, streaming vs polling lag, versioned updates and outage fallbacks
// Feature flag delivery at scale: remote vs local evaluation, streaming vs polling,
// out-of-order updates, and what an SDK serves when the flag service is down.

// ---- 1. Capacity: why SDKs evaluate locally (all inputs are assumptions) ----
const instances = 2000, rpsPerInstance = 500, flagsPerRequest = 5;
const flags = 1500, bytesPerFlag = 2000, changesPerDay = 300, pollSeconds = 30;
const evalsPerSec = instances * rpsPerInstance * flagsPerRequest;
const configMB = (flags * bytesPerFlag) / 1e6;
console.log("== 1. capacity ==");
console.log(`  flag evaluations: ${evalsPerSec.toLocaleString("en-US")}/s -> remote evaluation = that many network calls/s`);
console.log(`  local evaluation: ${instances} long-lived streams, ${configMB} MB of rules per SDK, 0 calls per evaluation`);
console.log(`  full restart of the fleet downloads ${(instances * configMB / 1000).toFixed(1)} GB of rules`);
console.log(`  change fan-out: ${changesPerDay} changes/day x ${instances} streams = ${(changesPerDay * instances).toLocaleString("en-US")} messages/day`);
console.log(`  polling every ${pollSeconds} s instead: ${(instances / pollSeconds).toFixed(1)} requests/s, ${((86400 / pollSeconds) * instances).toLocaleString("en-US")}/day, mostly unchanged`);

// ---- 2. Propagation of one change (fake clock, milliseconds) ----
// The flag "checkout-v2" is flipped to false (kill switch) at t = 10,000 ms.
const changeAt = 10000;
const sdks = [
  { name: "stream-a", mode: "stream", latency: 40 },
  { name: "stream-b", mode: "stream", latency: 180 },
  { name: "stream-c", mode: "stream", latency: 90, disconnectedUntil: 18500 }, // reconnects, then re-syncs
  { name: "poll-a", mode: "poll", offset: 3000 },
  { name: "poll-b", mode: "poll", offset: 21000 },
  { name: "poll-c", mode: "poll", offset: 9900 },
];
function receivedAt(s) {
  if (s.mode === "stream") return Math.max(changeAt + s.latency, s.disconnectedUntil ? s.disconnectedUntil + s.latency : 0);
  let t = s.offset; // polls at offset, offset + 30 s, ...
  while (t < changeAt) t += pollSeconds * 1000;
  return t;
}
console.log("== 2. time until each SDK serves the new value ==");
let worst = { stream: 0, poll: 0, reconnect: 0 };
for (const s of sdks) {
  const lag = receivedAt(s) - changeAt;
  const k = s.disconnectedUntil ? "reconnect" : s.mode;
  worst[k] = Math.max(worst[k], lag);
  console.log(`  ${s.name.padEnd(9)} ${String(lag).padStart(6)} ms${s.disconnectedUntil ? "  (stream was down; old value served meanwhile)" : ""}`);
}
console.log(`  worst healthy stream: ${worst.stream} ms   reconnecting stream: ${worst.reconnect} ms   worst poll: ${worst.poll} ms`);

// ---- 3. Out-of-order updates: apply only if version is newer ----
function apply(store, update, checkVersion) {
  const cur = store.get(update.key);
  if (checkVersion && cur && cur.version >= update.version) return "ignored";
  store.set(update.key, update);
  return "applied";
}
const arrivals = [
  { key: "checkout-v2", version: 8, value: false }, // the kill switch
  { key: "checkout-v2", version: 7, value: true },  // older patch delayed in a retry
];
console.log("== 3. updates arriving out of order (v8 kill switch, then a delayed v7) ==");
for (const check of [false, true]) {
  const store = new Map([["checkout-v2", { key: "checkout-v2", version: 6, value: true }]]);
  const res = arrivals.map((u) => `v${u.version}:${apply(store, u, check)}`).join(" ");
  const f = store.get("checkout-v2");
  console.log(`  ${check ? "with version check   " : "without version check"} ${res} -> serving v${f.version} value=${f.value}`);
}

// ---- 4. Flag service outage: what does each SDK serve? ----
// Last value in the service: checkout-v2 = true. Code default: false.
const codeDefault = false;
const cases = [
  { name: "warm SDK, stream drops", store: { "checkout-v2": true } },
  { name: "cold start, no persistent store", store: null },
  { name: "cold start, Redis or relay cache", store: { "checkout-v2": true } },
];
console.log("== 4. flag service down ==");
for (const c of cases) {
  const v = c.store ? c.store["checkout-v2"] : codeDefault;
  const why = c.store ? "last known value" : "code default";
  console.log(`  ${c.name.padEnd(34)} serves ${String(v).padEnd(5)} (${why})`);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 54 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

How would you design a rate limiter (token bucket vs leaky bucket)?

Continue