Articles
Feature Flag System Design Interview Guide: Rollouts, Consistency, and Auditability
Design feature flags with tested cross-SDK buckets, stable rollout cohorts, versioned rollback, stale-client defaults, governance and audit history.
A strong feature flag system design interview answer separates two workloads that look related but behave very differently: a low-write, heavily governed control plane and a high-read, latency-sensitive data plane. Admin changes should be validated, versioned, authorized, and auditable. Runtime evaluations should usually happen from a local cache so an application can keep serving traffic when the flag service is slow or unavailable.
Feature flag system: a service that stores release rules separately from application deployments, distributes versioned configuration to runtimes, and evaluates a flag for a user, tenant, device, or service. It supports targeted rollouts, rapid disablement, experiments, and operational controls without requiring a new code deployment.
This guide is for software engineers preparing for a system design round.
Clarify the requirements before drawing boxes
Start by asking what the flag controls. A release flag for a redesigned button has different failure semantics from an entitlement, fraud rule, or security control. Clarify offline evaluation, cross-device stickiness, and whether several flags must share one cohort.
Then establish interview assumptions. A reasonable example uses 100,000 evaluations per second, 50 admin changes per minute, 20,000 active flags, three regions, and a 30-second propagation target. These are sample inputs, not industry constants; they show why evaluation should avoid cross-region database calls.
Functional requirements usually include:
- Create, edit, approve, schedule, pause, archive, and roll back flags.
- Target users, accounts, devices, services, segments, and percentage cohorts.
- Evaluate boolean or multivariate flags with an explicit default.
- Preserve sticky assignments during a gradual rollout.
- Search who changed a production flag, what changed, why, and which revision replaced it.
Non-functional requirements should cover evaluation latency, data-plane availability, propagation delay, tenant isolation, access control, audit retention, and recovery from stale or malformed configuration.
Separate the control plane from the data plane
The control plane owns authoring and governance. An Admin API authenticates the caller, checks role and environment permissions, validates ordered targeting rules, and optionally requires approval for production. A successful mutation creates a new immutable configuration revision and a corresponding audit event in one logical transaction.
The data plane distributes and evaluates accepted revisions. A publisher emits the revision to regional relays; SDKs receive updates through a stream or jittered polling and store the newest snapshot in memory. Evaluation becomes a pure function of the snapshot, flag key, and evaluation context.
This split is visible in real systems. AWS AppConfig documents separate control- and data-plane APIs, while Unleash documents local evaluation in backend SDKs. The interview conclusion is an architectural synthesis: keep administration strongly governed, but keep the application hot path independent of a remote control-plane request.
Evaluation telemetry should use a separate asynchronous channel. If analytics is slow, buffer within a fixed memory limit or sample and drop events according to policy; never make a flag lookup wait for a warehouse.
Model flags, rules, snapshots, and revisions explicitly
A useful model contains FlagDefinition, Variation, ordered TargetingRule, Rollout, immutable ConfigSnapshot, ApprovalRequest, and AuditEvent.
Each rollout needs a percentage, bucketBy attribute, and salt. Each snapshot needs a revision, checksum, and the configuration needed by an SDK. Audit events preserve old and new revisions, actor, reason, approval reference, time, and request ID.
Make rule order unambiguous: prerequisites, explicit targets, conditional rules, percentage rollout, then default. Detailed output should include the variation, revision, and reason. OpenFeature's evaluation API similarly supports typed values, caller-supplied defaults, and troubleshooting details.
Cross-SDK Fixture: Grow a Rollout Without Moving the First Cohort
Use one concrete flag to test your design. The flag is checkout-v2, its salt is pilot, and its targeting key is the stable account ID. A boolean true means the new checkout. The default is false. Revision 41 enables the first 5,000 of 100,000 buckets; revision 42 raises that threshold to 25,000 without changing the hash inputs.
The hash contract needs more detail than “use a consistent hash.” Specify encoding, input boundaries, digest, byte order and bucket range. Our fixture encodes each of the three strings as UTF-8 and prefixes each with its byte length and a colon. It hashes the concatenation with SHA-256, reads the first eight digest bytes as an unsigned big-endian integer and reduces modulo 100,000. Buckets are zero-based, from 0 through 99,999. There is no implicit Unicode normalization, case folding or whitespace trimming. Require well-formed Unicode input at the API boundary.
Length prefixes distinguish ("ab", "c", "d") from ("a", "bc", "d"). A byte length also handles a name such as José correctly: characters and UTF-8 bytes are different counts. Do not use a runtime's default hash function; it may have a different algorithm or seed on another machine.
import hashlib
def bucket(flag, salt, subject):
parts = [value.encode("utf8") for value in (flag, salt, subject)]
payload = b"".join(str(len(part)).encode("ascii") + b":" + part
for part in parts)
digest = hashlib.sha256(payload).digest()
return int.from_bytes(digest[:8], "big") % 100000
The equivalent Node.js implementation must use BigInt; a JavaScript Number cannot represent every 64-bit integer exactly. Convert only the final bucket to a number.
const { createHash } = require("node:crypto");
function bucket(flag, salt, subject) {
const parts = [flag, salt, subject].map(value => Buffer.from(value, "utf8"));
const payload = Buffer.concat(parts.flatMap(part => [
Buffer.from(String(part.length) + ":", "ascii"), part
]));
const digest = createHash("sha256").update(payload).digest();
return Number(digest.readBigUInt64BE(0) % 100000n);
}
Golden vectors and expected variants
Both implementations should produce these exact buckets for checkout-v2 and pilot. Ship the fixture to every SDK implementation, along with empty, non-ASCII and boundary cases. The four accounts below are deliberately chosen to show different outcomes; they are not a representative traffic sample.
| Targeting key | Bucket | R41: 5% | R42: 25% | Bucket with salt pilot-2 |
|---|---|---|---|---|
| u007 | 787 | true | true | 15494 |
| u008 | 10803 | false | true | 94750 |
| u000 | 54456 | false | false | 26163 |
| José | 91252 | false | false | 76009 |
With unchanged inputs, every bucket enabled at 5% remains enabled at 25%. Changing the salt to pilot-2 changes membership: u007 would leave a 5% rollout and u008 would leave a 25% rollout. A salt change is a deliberate cohort reset, not a routine revision increment. If two flags must share one experiment cohort, use an explicit shared cohort contract rather than assuming separately seeded flags correlate.
Test the threshold comparison directly: bucket 4,999 is enabled at 5%, bucket 5,000 is not, bucket 24,999 is enabled at 25%, and bucket 25,000 is not. Weight 0 enables none; weight 100,000 enables all. Reject an invalid weight before distributing the snapshot. Return an explicit fallback reason if the required targeting key is absent; do not invent a fresh random identity on every request and call it sticky.
Revision Trace: Rollback Is a New Write
Now u008 is receiving the new checkout under revision 42. A guardrail breaches, so an operator restores the prior 5% configuration. Publish that configuration as revision 43, with an audit event recording that it restores revision 41. Sending revision 41 again would collide with the rule that clients must never install older state.
| Event | SDK A | SDK B | Expected evaluation for u008 |
|---|---|---|---|
| R41 accepted | R41 installed | R41 installed | Both false |
| R42 accepted | R42 installed | Stream disconnected; R41 cached | A true; B false while its snapshot is valid |
| R43 restores 5% | R43 installed | Still disconnected | A false; B follows its stated stale policy |
| Delayed R42 reaches A | Reject revision 42 | Still R41 | A stays false at R43 |
| B reconnects | R43 | Install R43 directly | Both false at R43 |
The configuration revision is monotonic even though the rollout percentage decreases. Duplicate delivery of revision 43 is harmless only if its checksum matches the installed payload. A different checksum for the same revision is an integrity conflict: keep the valid snapshot, alert and recover from an authoritative source. Do not use “latest arrival wins.”
For this mock, a snapshot is valid for 30 seconds since successful authoritative freshness confirmation. Beyond that limit, return false, reason STALE_DEFAULT, and expose the last observed revision for diagnosis. A cold start also returns false, with NOT_INITIALIZED. These are chosen exercise policies, not universal SDK behavior. Use a local monotonic clock for elapsed age; a valid no-change response can refresh freshness, while an arbitrary transport message cannot.
An alternative policy could keep serving the last-known-good release flag indefinitely. That may improve availability, but it cannot promise an enforced 30-second disable bound. Explain the trade-off. Security-critical authorization belongs in an appropriate authoritative enforcement path; an offline release-flag SDK is not a substitute for that decision.
The control plane's “rollback accepted” message is therefore not proof that every client changed. Measure installed revision and fallback reason across required runtimes. Report the affected fraction and oldest valid snapshot, then declare the operational objective met according to an explicit threshold. Do not put account IDs in metric labels: aggregate by region, SDK version and revision while keeping cardinality bounded.
A small reviewer checklist
Ask a peer to change exactly one condition: Unicode encoding, missing identity, out-of-order delivery, a disconnected client or an altered salt. Your explanation should predict the output before running the fixture. A strong answer names the contract that changed and the smallest corresponding fix. “Use caching and retries” does not explain why the wrong revision cannot take over after rollback.
Design auditability and access control as core features
Production flag changes can alter behavior without a deploy, so they need the same governance as other operational changes. Use least-privilege roles by project and environment, separate draft from production approval, require a reason or ticket for sensitive changes, and support break-glass access with extra review.
Write the primary audit event atomically with the accepted mutation. Export asynchronously; an export failure must not erase the source record. Protect events from alteration, apply retention rules, and audit access to the audit system.
Official products expose similar evidence. LaunchDarkly maintains change history for feature flags and other resources, and Unleash documents an event log with actor, time, environment, and prior state. These sources support the capabilities, not a claim that one vendor's schema is required for your design.
Temporary flags also need owners, expiry dates, code references, and stale-flag alerts. Auditability is not complete if the system records changes forever but allows obsolete branches to accumulate in application code.
Measure propagation and guardrail health
Track evaluation latency, fallbacks, configuration age, propagation delay, SDKs on the latest revision, reconnects, polling QPS, and audit lag. Exposure counts validate rollout behavior, but raw user IDs are unsuitable metric labels because of privacy and cardinality.
Compare canary and control error rate, latency, saturation, and the business metric that the change intends to improve. Define a minimum sample or bake window, then pause or roll back on a sustained guardrail breach. Google's SRE Workbook describes canarying as a partial, time-limited deployment evaluated against a control; the exact thresholds remain an application decision.
A 45-minute interview walkthrough
Use the interview clock to make your reasoning visible:
- Minutes 0-5: clarify flag types, scale, identities, regions, propagation, and compliance.
- Minutes 5-10: state requirements and exclusions.
- Minutes 10-20: draw both planes, regional delivery, SDK cache, evaluator, and telemetry.
- Minutes 20-28: deep-dive on rule order, bucketing, stickiness, and test vectors.
- Minutes 28-35: explain revisions, staleness, recovery, defaults, and kill-switch limits.
- Minutes 35-42: cover RBAC, approvals, atomic audit records, metrics, and rollback.
- Minutes 42-45: summarize trade-offs and the next risk to investigate.
Avoid three common mistakes: calling random-per-request allocation a percentage rollout, putting a remote service call on every evaluation, and treating “eventual consistency” as permission to omit revision ordering and stale-state monitoring.
Frequently asked questions
Should feature flags be evaluated locally or remotely?
Server-side evaluation is usually local from a cached snapshot because it minimizes latency and survives control-plane outages. Remote evaluation can simplify thin clients and conceal sensitive targeting rules. State which client types use each model, how configuration reaches them, and what safe default applies before initialization.
Does a kill switch require strong consistency?
Not necessarily. Strong consistency can slow or couple every evaluation to a remote dependency. Many designs use fast versioned propagation with a measured time-to-effective-disable objective. If a requirement truly demands immediate enforcement, move that particular decision to an authoritative policy service rather than pretending an offline SDK is instantly consistent.
How do percentage rollouts keep users in the same cohort?
Hash a stable user, account, device, or tenant key together with flag-specific seed material, then map the hash to a fixed bucket. The same inputs produce the same bucket across requests. Document the canonical algorithm and ship cross-language test vectors so every SDK agrees.
What should happen when the feature flag service is unavailable?
Existing processes can use their last-known-good snapshot while it satisfies the selected age policy, expose its age, and reconnect with backoff. In this mock, a snapshot older than 30 seconds returns the explicit false default. A cold process also returns its code default. Choose the policy per flag and explain how it affects availability and disablement guarantees.
What belongs in a feature flag audit event?
Record the flag and environment, old and new revisions, before and after configuration, actor, authorization principal, approval or change-ticket reference, reason, timestamp, request ID, and permitted source metadata. Persist it with the mutation, then export asynchronously so downstream outages cannot lose the authoritative history.
Sources and Further Reading
- OpenFeature: Flag Evaluation API and defaults
- LaunchDarkly: Percentage rollouts
- LaunchDarkly: Change history
- Unleash: Architecture and local evaluation
- Unleash: Stickiness
- Unleash: Event Log
- AWS AppConfig: Configuration retrieval and data-plane endpoints
- AWS AppConfig: Monitoring for automatic rollback
- Google SRE Workbook: Canarying Releases
Editorial review: October 8, 2026. Validate the provider and client behavior you actually deploy.