Design problems
Platform & infrastructureMediumSDE-2 30 min

Design a feature flag and config service

Asked at
AtlassianFlipkartMeeshoMicrosoftSwiggyUber

The interviewer asks

“Design a feature flag and dynamic configuration service (like LaunchDarkly or an internal equivalent) used by 500 backend services and mobile apps with 50 million users. Teams should be able to turn features on for 1% of users, target by city or app version, and kill a bad feature within seconds.”

What a strong answer looks like

A strong answer separates the control plane (flag definitions, targeting rules, audit, approvals, stored in Postgres) from the data plane (evaluation). The key decision is to evaluate flags locally inside each service via an SDK that holds the full ruleset in memory, so a flag check is a sub-microsecond function call, not a network request. SDKs get updates through a streaming connection (SSE/gRPC) from a fan-out tier, with periodic polling and a local cache as a fallback, and they keep working on the last known config if the service is down. Percentage rollouts use deterministic hashing of (flag_key, user_id) into buckets so a user's experience is sticky and increasing 1% to 5% only adds users. Mobile and web clients cannot hold all rules (they leak targeting data), so they get server-evaluated results for one user from an edge evaluation API. Then cover safety: kill switches, staged rollouts linked to SLO alerts, approvals and audit, stale flag cleanup, and consistency (versions, propagation within ~5 s).

Concepts to mention

control plane vs data planelocal in-memory evaluation in server SDKsstreaming updates with versioned patches and polling fallbackdeterministic hashing for sticky percentage rollouts with per-flag saltserver-side evaluation for mobile and web clientssafe defaults and last-known-good snapshotkill switch, audit log, approvalsstaged rollouts tied to SLO alertsstale flag cleanup

1 Requirements

Functional

  • Create boolean, string, number and JSON flags and configs with a default value
  • Targeting rules: user id lists, attributes (city, app_version, platform, plan), segments, and percentage rollouts
  • Kill switch: turn a flag off globally, propagated within seconds
  • Server SDKs (Go, Java, Node, Python) and client SDKs (Android, iOS, web)
  • Environments (dev, staging, prod) and per-environment rules
  • Audit log of every change with who, what, when and why; optional approval for prod changes
  • Exposure events (which user saw which variant) for experiments and debugging
  • Scheduled changes and automatic rollout steps (1% → 10% → 50% → 100%)

Non-functional

  • Flag evaluation in server SDKs under 1 microsecond-ish, no network call on the request path
  • Changes propagate to 95% of SDKs within 5 seconds, all within 60 seconds
  • Service outage must not break products: SDKs serve last known values or coded defaults
  • Control plane availability 99.9%; data plane (delivery) 99.99%
  • Scale: ~20,000 server SDK instances, ~5M concurrently connected mobile clients at peak
  • Consistent bucketing: the same user gets the same variant across services and sessions

Out of scope (say it out loud)

  • Statistical analysis of A/B test results (experiment analysis platform)
  • Secrets management (use Vault / a secrets manager, never flags)
  • Deploy pipelines and canary deployments of binaries

2 Estimates

Flag evaluations~10M/s across the fleet500 services × avg 40 instances × 500 req/s × ~1 flag per request ≈ 10M/s; must be local, not remote calls
Ruleset size per environment~2-5 MB~5,000 active flags × ~500 B-1 KB of rules each; fits easily in every SDK's memory
Change rate~500-2,000 changes/dayHundreds of teams doing rollout steps; write load is tiny, delivery fan-out is the challenge
Server SDK streams~20,000 long-lived connectionsOne stream per service instance; a handful of relay pods handle this easily
Mobile client fetches~30k-60k req/s at peak50M users, ~10M daily opens peaking; each app start + refresh every 5-15 min; cached at the edge per user context
Exposure events~200k events/s before dedupSample or dedupe per (user, flag, variant) per hour in the SDK; send to Kafka for analysis

3 API

EndpointWhat it does
PUT/admin/v1/projects/{p}/envs/{env}/flags/{key}Create or update a flag: variants, default, rules, rollout; requires reason and optional approval; returns new version.
POST/admin/v1/flags/{key}/killKill switch: force the off variant in an environment, bypassing approvals, fully audited.
SSE/sdk/v1/stream?env=prodServer SDKs: first event is the full ruleset with version, then patch events (flag key, new version) as changes happen.
GET/sdk/v1/rules?env=prod (If-None-Match: version)Polling fallback and initial load; 304 if unchanged.
POST/client/v1/evaluateMobile/web: send user context (id, app_version, city), get evaluated values for that user only; signed client key.
POST/events/v1/exposuresBatched exposure and evaluation-count events from SDKs.

4 Data model

flagsPostgres
project_idenvkey (PK together)typevariants jsonbdefault_variantoff_variantrules jsonbrollout jsonbsaltversionowner_teamexpires_atarchived

Optimistic concurrency on version so two editors cannot overwrite each other. Rules are ordered; first match wins.

flag_changes (audit)Postgres (append-only) + export to audit system
change_idflag_keyenvactorbefore jsonbafter jsonbreasonapproved_byts

Needed for incident review: 'what changed at 14:02?'. Also drives rollback to any previous version.

segmentsPostgres; large lists in S3
segment_idrules or member listversion

Big lists (1M user ids) are stored as compact sets (e.g. sorted id files or bloom filters) and shipped separately so the main ruleset stays small.

ruleset snapshotS3/CDN + relay memory
envversioncompiled JSON of all flags

Built on every change; SDKs and relays can cold-start from it even if Postgres is down.

client evaluation cacheEdge cache / Redis
key = hash(env, user context)value = evaluated flagsshort TTL

Cuts repeated work for app starts; invalidated by ruleset version in the key.

exposuresKafka → ClickHouse/warehouse
tsflag_keyvariantuser_idserviceversion

Feeds experiment analysis and 'who saw variant B?' debugging.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAedit / kill flagwrite + auditpublish snapshotflag changed vNSSE patchescold start / pollevaluate(user ctx)reload rulesexposuresEngineers / PMs (webUI)Mobile / web appsServices + server SDKEdge / CDNControl plane APIClient evaluation APIStreaming relay (SSE)Change bus(Kafka/NATS)Exposure pipelinePostgres (flags,audit)S3 ruleset snapshotsClickHouse exposures
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Roll out a feature to 5% of Bengaluru users

  1. 1A PM edits flag new-checkout in prod: rule 1 'city = Bengaluru' → rollout {on: 5%, off: 95%}; default off. Approval by a second engineer is required for prod.
  2. 2The control plane validates the rules, writes the new version to Postgres with an audit row in the same transaction, and builds a new ruleset snapshot in S3.
  3. 3It publishes 'new-checkout changed, version 812' to the change bus.
  4. 4Every relay pod receives it and pushes a patch event over SSE to the ~20,000 connected server SDKs within about a second.
  5. 5Each SDK swaps the flag atomically in memory (copy-on-write map) and records the new version.
  6. 6On the next request, checkout calls isEnabled('new-checkout', user). The SDK evaluates rules locally: city matches; bucket = hash(salt + user_id) mod 10,000 = 312 < 500, so on.
  7. 7The SDK records an exposure (deduped per user/flag/variant per hour) and sends it in a batch to the event pipeline.
  8. 8The rollout scheduler later moves 5% → 25% only if checkout error-rate and latency SLOs stay healthy; users already in the 5% stay in.

Kill a bad feature during an incident

  1. 1On-call sees payment errors spike right after new-checkout reached 25% and presses Kill in the UI or CLI.
  2. 2Kill bypasses approval but requires a reason; the off variant is forced for the environment, a new version is written and broadcast.
  3. 3Server SDKs get the patch in 1-3 s; mobile clients get it on the next evaluate call or via a silent push asking them to refresh.
  4. 4The audit log and an automatic incident annotation on dashboards show exactly when and by whom.

7 Deep dives

Local evaluation and the delivery path

The most important design choice is that server-side flag checks never call the flag service. At ~10M evaluations per second, even a 1 ms remote call would add latency to every request and make the flag service a single point of failure for the whole company. Instead, each SDK downloads the full ruleset for its environment (a few MB), keeps it in memory, and evaluates rules as a pure function. Delivery then becomes a fan-out problem: a change written once must reach about 20,000 SDK instances quickly. Use a streaming relay tier: SDKs hold a long-lived SSE (or gRPC stream) connection; on connect they receive the full ruleset with a version number, then small patches. Relays subscribe to a change bus, so adding relay pods scales delivery. Patches carry versions, and an SDK that sees a gap (version 810 then 812) re-fetches the full snapshot. If the stream drops, SDKs reconnect with jittered backoff and poll the snapshot (with ETag) every 30-60 s as a fallback. On cold start with the service completely down, SDKs load the last snapshot from S3/CDN or a local file baked into the image, and finally fall back to the default value passed in code. The flag service can be fully down and production keeps working on the last known config.

Deterministic percentage rollouts

A percentage rollout must be sticky (the same user always gets the same variant), consistent across services (checkout and the order page agree), and monotonic (going from 5% to 25% keeps the original 5% in). Do not use random numbers or store per-user assignments. Hash instead: bucket = murmur3(flag_salt + ':' + user_id) mod 10,000, giving buckets 0-9,999 with 0.01% granularity. Variant 'on' owns buckets 0 to (percent × 100 - 1). Raising the percent only extends the range, so existing users stay in. Each flag has its own salt; if all flags hashed only the user id, the same 1% of users would get every new feature at once and experiments would be correlated. For experiments that must be independent of a previous rollout, change the salt (re-randomise) on purpose. Choose the bucketing key carefully: user_id for logged-in features, device_id for logged-out users, or tenant_id/restaurant_id when a feature must apply to a whole business. Note that a user who logs in mid-session may switch buckets if you change keys, so define it per flag.

Client-side flags for mobile and web

Mobile apps cannot receive the full ruleset: it would leak targeting data (list of beta users, unreleased feature names, enterprise customers) and could be large. So clients use server-side evaluation for one user: the app sends its context (user id, app version, city, platform) with a client-side key to an evaluation API at the edge, and receives only the final values. The app caches them on disk and uses them on next start, which matters on Tier-2/3 networks where the first request may take seconds. Responses can be cached at the CDN per context hash with a short TTL. Updates reach clients on app start, foreground and every 5-15 minutes, or via a streaming connection for web; for a kill switch you can send a silent push. Two mobile-specific traps: old app versions live for months, so every flag needs a safe default compiled into the binary; and a flag that changes UI mid-session can confuse users, so many apps 'freeze' flag values for the session and apply new ones on next launch, except kill switches.

Safety: flags can cause outages too

Many large incidents come from config changes, not code deploys, because config often skips the testing that code gets. Treat flag changes like deploys: validation (type checks, a JSON schema for config flags, limits like 'timeout_ms between 50 and 10,000'), required reason and approval in prod, staged rollouts linked to SLO metrics with automatic halt and rollback, and a one-click revert to any previous version from the audit log. Keep kill switches simple and always available, without approvals. Limit blast radius by environment and by region (enable in one region first). Clean up: every flag has an owner and an expiry; the system flags stale ones (100% on for 30 days, or not evaluated for 14 days) and opens a ticket to remove the code path, because old flags become a combinatorial testing problem. Never put secrets in flags; ruleset snapshots are widely distributed.

8 Trade-offs

Local evaluation vs remote evaluation

Remote evaluation (call the service per check) is simple, keeps rules secret, and changes apply instantly, but adds latency and a hard runtime dependency. Local evaluation is fast and resilient but needs SDKs in every language, a delivery system, and accepts a few seconds of staleness. Use local for servers, remote (edge-cached) for untrusted clients.

Streaming vs polling

Polling every 30 s is simple and robust behind any proxy, but changes take up to 30 s and 20,000 pollers create steady load. Streaming delivers in about a second with tiny load per change, but needs long-lived connections, reconnect logic and proxies configured not to buffer. Use streaming with polling as a fallback.

Build vs buy

LaunchDarkly, Unleash, Flagsmith, ConfigCat and cloud services (AWS AppConfig) solve most of this. Building makes sense for very large companies that need deep integration (experiments, data localisation, cost at scale); otherwise use OpenFeature as the SDK interface so the backend can be swapped.

9 Bottlenecks

Reconnect storm after a relay deploy

20,000 SDKs reconnecting at once all request the full ruleset. Use jittered backoff in SDKs, drain relays slowly during deploys, and serve full snapshots from a CDN/S3 with ETags rather than from Postgres.

Huge segments

A 'beta users' list of 2M ids inside the ruleset makes every SDK download huge. Store big lists separately, ship them as compact sets only to services that need them, or evaluate those flags server-side.

10 Failure handling

Flag service and Postgres fully down

Server SDKs continue with in-memory rules; new pods start from the S3/CDN snapshot or a bundled file; worst case they use code defaults. Only flag changes are impossible during the outage, which is why the kill-switch path should also have a break-glass option (e.g. directly publish a signed snapshot).

Bad config flag pushed (timeout set to 0)

Schema validation should have rejected it; if not, SLO-linked rollout halts the change in the first stage, and one-click revert restores the previous version within seconds. Postmortem adds a validation rule.

Some SDKs miss an update

SDKs report their ruleset version in a heartbeat; the control plane shows how many instances are on old versions and SDKs with version gaps re-fetch the full snapshot automatically.

11 What impresses

  • Refuses to put a network call on the request path and explains the 10M evaluations/s math
  • Explains monotonic, sticky bucketing with hash(salt + user_id) and why each flag needs its own salt
  • Separates server and client SDK models, with the data-leak reason
  • Treats config changes as a major outage source and adds validation, staged rollout and instant revert
  • Describes how SDKs behave when everything is down: snapshot, then code defaults
  • Mentions flag debt and an expiry/cleanup process

12 Mistakes

  • Calling the flag service over the network for every evaluation
  • Using random() for percentage rollouts, so users flip between variants
  • Shipping the full ruleset, including targeting lists, to mobile apps
  • No defaults when the SDK cannot reach the service
  • No audit or approval, so nobody knows which flag change caused an incident
  • Using flags to store secrets

13 Follow-ups

Q1How do you guarantee two services agree on a user's variant in the same request?
Q2How would you run an A/B test on top of this and make sure assignment is not biased?
Q3How do you push a kill switch to mobile apps that are offline?
Q4How would you support per-tenant configuration for a B2B SaaS with 10,000 tenants?
Q5How do you detect and remove flags that are no longer needed?
Q6What changes if the ruleset grows to 200 MB?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free