Design a feature flag and config service
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
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
3 API
| Endpoint | What 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}/kill | Kill switch: force the off variant in an environment, bypassing approvals, fully audited. |
SSE/sdk/v1/stream?env=prod | Server 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/evaluate | Mobile/web: send user context (id, app_version, city), get evaluated values for that user only; signed client key. |
POST/events/v1/exposures | Batched exposure and evaluation-count events from SDKs. |
4 Data model
flagsPostgresproject_idenvkey (PK together)typevariants jsonbdefault_variantoff_variantrules jsonbrollout jsonbsaltversionowner_teamexpires_atarchivedOptimistic 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 systemchange_idflag_keyenvactorbefore jsonbafter jsonbreasonapproved_bytsNeeded for incident review: 'what changed at 14:02?'. Also drives rollback to any previous version.
segmentsPostgres; large lists in S3segment_idrules or member listversionBig 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 memoryenvversioncompiled JSON of all flagsBuilt on every change; SDKs and relays can cold-start from it even if Postgres is down.
client evaluation cacheEdge cache / Rediskey = hash(env, user context)value = evaluated flagsshort TTLCuts repeated work for app starts; invalidated by ruleset version in the key.
exposuresKafka → ClickHouse/warehousetsflag_keyvariantuser_idserviceversionFeeds experiment analysis and 'who saw variant B?' debugging.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Roll out a feature to 5% of Bengaluru users
- 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.
- 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.
- 3It publishes 'new-checkout changed, version 812' to the change bus.
- 4Every relay pod receives it and pushes a patch event over SSE to the ~20,000 connected server SDKs within about a second.
- 5Each SDK swaps the flag atomically in memory (copy-on-write map) and records the new version.
- 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.
- 7The SDK records an exposure (deduped per user/flag/variant per hour) and sends it in a batch to the event pipeline.
- 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
- 1On-call sees payment errors spike right after new-checkout reached 25% and presses Kill in the UI or CLI.
- 2Kill bypasses approval but requires a reason; the off variant is forced for the environment, a new version is written and broadcast.
- 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.
- 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
Lessons this design uses
Practice questions
- How do feature flags make releases safer, and what new risks do they add? Give an example from an e-commerce sale.
- A one-line config change pushed to all servers globally in 30 seconds took down your whole platform. Code deploys go through canaries, but config did not. How would you design safe config delivery?
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.