System design interview

Design a feature flag service

Local rule evaluation, sticky percentage rollouts, kill switches and audit.

At a glance

Level
Foundation
Topics
Consistency, Caching
Main parts
Flag API · Flag store · Publisher · Ruleset cache · Stream server · App SDK
What you'll learn
Practise this problem
Overview
Admin UIFlag APIFlag storePublisherRulesetcacheStreamserverApp SDKYour appAdmin UIFlag APIvalidates, savesFlag storeversions, auditPublisherbuilds rulesetsRuleset cacheone file per envStream serverpushes deltasApp SDKrules in memoryYour app

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. An engineer raises the rollout of a flag from 10% to 25% in the admin UI.
  2. The API checks the change against the rules schema, then saves a new immutable version and an audit record.
  3. The API tells the publisher that the production environment has a new version.
  4. The publisher reads the current rules for that environment and compiles them into one compact snapshot.
  5. It writes the snapshot, labelled with its version number, to the ruleset cache.
  6. It also hands the stream server a small delta that holds only the changed flag.
  7. The stream server pushes the delta to every connected SDK, which applies it to the rules in memory.
  • Admin UI
  • Flag API: validates, saves
  • Flag store: versions, audit
  • Publisher: builds rulesets
  • Ruleset cache: one file per env
  • Stream server: pushes deltas
  • App SDK: rules in memory
  • Your app
On this page

In 2 minutes

Key ideas
  • The numbers. Reads are huge and writes are tiny. So move the rules to the readers: keep them in each app's memory and evaluate there.
  • Where flags are evaluated. Locally in an SDK, from rules in memory.
  • How changes arrive. A stream, with polling as a fallback.
  • Percentage rollouts. A hash of the flag and the user key.
  • Clients in browsers and phones. Evaluate on the server and send only the results.
Read the 2-minute answer

The key choice is to move the rules to the readers. An app instance may check ten flags on every request, which at our assumed scale is 100 million checks a second, so a network call per check is out. Each app embeds an SDK that holds the whole ruleset, about 2 MB, in memory and evaluates locally in microseconds. Changes flow from an admin UI through a flag API into a store that keeps immutable, numbered versions and an audit log.

A publisher builds a snapshot for a CDN-backed cache and hands a small delta to a stream server, which pushes it to every SDK in about a second. Polling every 30 seconds is the safety net, and a version gap triggers a full fetch. If the service is down, the SDK keeps its last rules, so nothing fails. Percentage rollouts hash the flag key and the user key into one of 10,000 buckets, so a user stays in as the percentage grows.

A kill switch takes the same fast path with no batching. The trade-off is that rules live in every app, so SDKs must agree on the hash and validate every snapshot.

Step 1 of 6: Clarify the scope

Questions to ask first

Ask these before you draw anything. Guess why each answer changes the design, then open it to check.

Who asks for flags: backend servers, web browsers, mobile apps, or all three?
Why it matters: A server can hold the full rules in private. A browser or a phone cannot, because anyone could read the targeting rules from it.
How rich are the targeting rules: plain on or off, lists of users, user attributes, percentages?
Why it matters: The richer the rules, the bigger the ruleset and the more careful the evaluator must be.
How fast must a change reach the apps?
Why it matters: Seconds means a pushed stream of changes. A few minutes allows plain polling, which is simpler.
What should an app do if the flag service is down?
Why it matters: The answer sets the caching rules. The app should keep working with the last rules it saw, or with a safe default.
Do we need approvals, history and an audit trail?
Why it matters: A flag change alters production behaviour, so most teams want to know who changed what, and to undo it.

Requirements

Functional

  • Create flags with on or off values, targeting rules and percentage rollouts.
  • Evaluate a flag for a given user inside the app, with a safe default if anything is missing.
  • Push every change, including an emergency kill switch, to all connected apps.
  • Keep a version history and an audit trail of who changed what and when.

Non-functional

  • A flag check adds under 1 ms at p99 and makes no network call.
  • 99% of connected apps see a change within 5 seconds.
  • Apps keep working with their last known rules while the service is down.
  • A user who is in a rollout stays in it when the percentage grows.

Step 2 of 6: Size it

Show the maths

These numbers are assumptions for the exercise, not facts about a real system:

  • 20,000 app instances run the SDK. At peak each handles 500 requests a second and checks about 10 flags per request.
  • There are 2,000 flags in one environment, and compact rules average about 1 KB per flag.
  • Teams make about 200 flag changes a day. Each touches one flag, so a change is a delta of about 1 KB.
  • A local evaluation takes about 2 microseconds. A call to a remote service in the same region takes about 1 ms.

The arithmetic:

Flag checks per second
20,000 instances × 500 requests/s × 10 flags
100 million/s in total, far too many for a remote call each
Size of all rules
2,000 flags × 1 KB
≈ 2 MB, small enough to hold in every app's memory
Cost per request, local
10 flags × 2 µs
20 µs, about 0.02 ms
Cost per request, remote
10 flags × 1 ms, one call each
≈ 10 ms added to every request
One change, full ruleset pushed
20,000 instances × 2 MB
40 GB for a one-line change
One change, delta pushed
20,000 instances × 1 KB
20 MB, 2,000 times less

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

People change flags in the Admin UI, which calls the Flag API. The API validates the change and saves it to the Flag store, together with a new version number and an audit record. Nothing in the request path of any app depends on this part.

The Publisher is told about the change. It reads the current rules from the store, builds a versioned snapshot for each environment and writes it to the Ruleset cache, which is a file store behind a CDN. It also hands the change to the Stream server, which pushes a small delta to every connected app.

Each application embeds an App SDK. On start it downloads the snapshot and keeps it in memory. It opens a stream to hear about changes, and polls the cache as a fallback. When the Your app code asks whether a flag is on for a user, the SDK answers from memory with no network call.

Other paths through the system

Start up and check a flag

  1. A new app instance starts. Its SDK asks the ruleset cache for the latest snapshot.
  2. The cache returns the snapshot and its version number, and the SDK loads it into memory.
  3. The SDK opens a stream to the server and says which version it holds, so it only receives newer changes.
  4. The SDK reports that it is ready, and the app starts to take traffic.
  5. A request arrives. The app asks the SDK whether the new checkout is on for user 42, with a default of off.
  6. The SDK evaluates the rules in memory, with no network call, and answers in microseconds.

The service is unreachable

  1. The stream drops. The SDK tries to reconnect, waiting a random time so thousands of apps do not retry together.
  2. It falls back to polling the ruleset cache, and the poll fails too.
  3. The SDK keeps answering the app from the last ruleset it holds. Flags stay as they were, and no request fails.

Step 4 of 6: Go deep on the hard part

Local evaluation and delivery

InterviewerHow does an app check a flag in microseconds, and how do changes and outages reach it?The core idea: ship the rules to the app, not the question to the service. The SDK keeps the whole ruleset in memory.

The core idea: ship the rules to the app, not the question to the service. The SDK keeps the whole ruleset in memory. Evaluation is a pure function. It takes the flag's rules and a context, such as the user ID and country, and returns a value. There is no network call, so the check cannot be slow and cannot fail because of the service.

Delivery has two paths. On start, the SDK downloads the latest snapshot from the cache, which is a static file that a CDN serves cheaply. After that it learns about changes in one of two ways:

  • A stream. The SDK holds a long-lived connection, such as server-sent events. The server pushes a small delta when a flag changes. Changes arrive in about a second. The cost is 20,000 open connections, which at an assumed 10,000 per node is three nodes with one spare.
  • Polling. Every 30 seconds the SDK asks the cache for the latest version, sending the version it holds. If nothing changed, the answer is a tiny 'not modified'. It is simple and works through any proxy, but a change can take up to 30 seconds. At 20,000 instances, that is about 670 requests a second.

Use both. The stream gives speed, and the poll is the safety net if a push is lost. Each delta carries a version number. If the SDK sees a gap, say it holds version 411 and receives 413, it fetches the full snapshot rather than guess.

When the service is down, the SDK keeps its last ruleset. It can also write the ruleset to local disk, so an instance that restarts during an outage still starts with real rules. If it has nothing at all, it returns the default that the caller passed in. After a long outage, reconnects should wait a random time, so 20,000 apps do not hit the stream server in the same second.

Browsers and phones are different. Shipping the full rules would leak targeting details, such as staff lists. So the server evaluates for that one user and sends back only the resulting flag values, which the client caches.

Sticky percentage rollouts

InterviewerHow do you turn a flag on for 10% of users so that a user's experience stays the same as you raise it?Do not pick randomly on each request.

Do not pick randomly on each request. A user would see the new checkout on one page and the old one on the next. Instead, give every user a fixed position and compare it to the percentage.

The position is a bucket. Hash the flag key and the user key together, and take the result modulo 10,000. The same inputs always give the same bucket. A user is in the rollout when their bucket is below the percentage times 100. At 10% the cut-off is 1,000, and at 25% it is 2,500.

A worked example. User 42 hashes to bucket 1,834 for this flag. At 10% the cut-off is 1,000, so they are out. When the team raises the flag to 25%, the cut-off is 2,500 and they are in. Nobody who was in at 10% is removed, because the buckets below 1,000 are still below 2,500. Growing a rollout only adds people. Lowering it removes the people with the highest buckets first.

Details that matter:

  • Include the flag key in the hash. If every flag hashed only the user, the same 10% of users would be first in line for every experiment.
  • Use a stable key. A user ID works. A session ID or an IP address changes, so the person flips in and out. For signed-out visitors, store a random device ID in a cookie.
  • Use the same hash in every SDK. A Java server and a JavaScript server must put user 42 in the same bucket. Pick one simple, non-cryptographic hash, write down the exact bytes that go into it, and publish test vectors that every SDK must pass.
  • Allow a reseed. A salt field on the flag lets you re-draw the groups on purpose, for a new experiment.

Rules run in order: a user on the blocked list first, then specific users, then attribute rules such as country, then the percentage, then the default. For a flag with three variants, split the 10,000 buckets by weight, say 0 to 4,999, 5,000 to 7,999 and 8,000 to 9,999.

Kill switches, versions and the audit trail

InterviewerA bad release is hurting users. How does a kill switch reach every app in seconds, and how do you trust the change history?A kill switch is an ordinary flag that wraps risky code and defaults to the safe path.

A kill switch is an ordinary flag that wraps risky code and defaults to the safe path. Its value is that turning it off is fast and needs no deploy. Speed is the requirement, so the path must have no slow step. Budget it: saving the change takes about 50 ms, the publisher about 100 ms, and the push to three stream nodes with about 6,700 connections each takes under a second. The SDK applies it in microseconds. That is a couple of seconds end to end, well inside the 5-second target.

Keep the kill-switch path free of batching. A delay that groups changes for a few seconds saves nothing here, so changes go out the moment they are saved. The polling fallback sets the worst case at 30 seconds if a push is lost.

Versions. Every save creates a new immutable version of the environment's ruleset, with a number that only goes up. The SDK remembers which version it holds. To roll back, republish the old contents as a new version, for example 415 with the contents of 412. Never reuse a number, or an SDK that holds 413 could not tell what changed.

Make rollouts observable. Each SDK reports the version it runs in a periodic heartbeat. The dashboard can then say '19,800 of 20,000 instances are on version 415' and name the ones that are not. Without it, 'it should have reached everyone' is a guess.

Audit. Write an append-only record for every change: who, when, which flag, the value before and after, and a reason or ticket. Never edit or delete these rows. Add permissions per environment, so more people can edit staging than production, and an optional approval step for production changes. Validate rules against a schema when they are saved. The SDK should also refuse a snapshot that fails to parse and keep the last good one.

Step 5 of 6: Name the trade-offs

Where flags are evaluated
Chose
Locally in an SDK, from rules in memory
Over
A call to the flag service on every check

Checks take microseconds and survive an outage. The cost is that rules are copied to every app and every SDK language must give the same answer.

How changes arrive
Chose
A stream, with polling as a fallback
Over
Polling only

Changes land in about a second, and a lost push is repaired by the next poll. The cost is a fleet of long-lived connections to run.

Percentage rollouts
Chose
A hash of the flag and the user key
Over
A random draw on each request

A user keeps the same experience as the percentage grows. The cost is that you need a stable user key, and small audiences can land unevenly.

Clients in browsers and phones
Chose
Evaluate on the server and send only the results
Over
Send the full rules to the client

Targeting details stay private and the payload is small. The cost is a network call at start and a stale value until the client refreshes.

Step 6 of 6: Say what breaks next

Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.

How would you roll a flag out to 1% of users in one country first?
Add a rule that requires the country to match and then applies the percentage to the users who match. Rules are checked in order, so the same flag can have a staff rule first, then this country rule, then a default of off. Raise the percentage in steps while you watch errors and latency.
How do you stop old flags from piling up?
Give each flag an owner and a type, such as release, experiment or permanent. Alert the owner when a release flag has been fully on for a few weeks, and show flags that no SDK has evaluated recently. The real cleanup is deleting the old code path, so tie the flag to a ticket for that.
How do you test a flag change before it reaches production?
Give each environment, such as staging and production, its own ruleset and version history. Test in staging, then copy the change across with a review step. In production, start with a rule that targets only internal users.
What if someone saves a broken rule?
Validate it against a schema on save and reject it with a clear message. As a second guard, the SDK refuses any snapshot it cannot parse and keeps the last good ruleset. Since every version is kept, a rollback is one click.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.