backend · intermediate · 5d
Feature-flag service
Build a small flag service with targeting rules, percentage rollouts, and a typed SDK that evaluates flags client-side from a cached ruleset.
Deliverable
An API that serves a flag ruleset and an SDK where flagOn('x', user) returns a deterministic, percentage-correct boolean.
Milestones
0/2 · 0%- 01Cached, ETag'd ruleset
Model flags + rules and serve them over a cached, ETag'd endpoint.
Definition of done- GET returns the flag ruleset with an ETag; a matching If-None-Match returns 304 with no body.
- Flags and targeting rules are modelled with a typed, validated schema.
- 02Deterministic % rollout
Implement deterministic percentage rollout via hashing user id + flag key.
Definition of done- hash(userId + flagKey) buckets a user so the same user always gets the same answer and ~X% are enabled at X.
- Changing the rollout percent moves the boundary monotonically without reshuffling already-enabled users.
Rubric
| Junior | Mid | Senior | |
|---|---|---|---|
| Deterministic rollout | Percentage rollout uses Math.random() per request; the same user gets a different answer on consecutive calls and the enabled fraction is approximate at best. | hash(userId + flagKey) mod 100 buckets a user deterministically: the same user always resolves to the same bucket, ~X% are enabled at X, and monotonic bucket expansion means increasing the rollout never disables already-enabled users. | You reason about bucket collisions between flags: if you hash userId alone (not userId+flagKey), users in the 0–10% bucket are always the same people for every flag — a systematic bias that skews A/B results. Salting with flagKey breaks the correlation. You demonstrate this with a chi-squared distribution test on a simulated user population. |
| Ruleset caching & propagation latency | The SDK fetches the full ruleset from the API on every evaluation call; a high-traffic service adds a network round trip to every request. | The ruleset is served with an ETag; SDK clients cache it locally and revalidate with If-None-Match, receiving 304 (no body transfer) when unchanged — evaluation is in-process with no per-call network I/O. | You reason about kill-switch propagation latency: a cache TTL of 60s means a flag turned off for a security incident stays on for up to 60s in every SDK. Streaming (SSE push) eliminates that window but adds a persistent connection per SDK instance. You document the chosen tradeoff and the worst-case propagation delay under your polling interval. |
| Eval consistency & targeting correctness | Targeting rules are applied in arbitrary order; a user matching multiple rules gets a non-deterministic result depending on evaluation sequence. | Rules are evaluated in priority order with an explicit fallthrough to the percentage rollout; the same rule model and same user always produce the same boolean, and the schema is typed and validated so a malformed rule is rejected at write time, not at eval time. | You address the stale-ruleset window: an SDK client evaluating a cached ruleset while the server has already changed a rule produces a split-evaluation inconsistency — some users see the old behavior, some the new. You document when this is acceptable (gradual rollout) vs. when it is not (a security kill-switch needing propagation in seconds), and show how the streaming channel closes the gap. |
Reference walkthrough (spoiler)
Why hash(userId + flagKey): hashing userId alone creates systematic bucketing correlation — the same 10% of users are always in the first decile for every flag. This biases any A/B experiment that relies on independent treatment assignments. Salting with the flagKey decorrelates buckets across flags; salting with an experiment-id decorrelates across re-runs of the same flag.
Kill-switch propagation latency is the hidden SLO: a flag service is usually a background concern until a security incident requires turning something off immediately. Polling-based SDKs add up to one TTL of exposure after a kill-switch is flipped. SSE streaming eliminates that window at the cost of a long-lived connection per SDK instance — the tradeoff is connection count (fan-out) vs. propagation time.
ETag caching for ruleset distribution: serving a versioned ETag with the ruleset and accepting If-None-Match means unchanged rulesets transfer only 200 bytes of headers, not the full payload. This makes frequent polling cheap and lets many SDK instances revalidate simultaneously without bandwidth spikes — the right default for polling-based SDKs before adding streaming.
Make it senior
- Add a streaming update channel (SSE) so SDKs refresh without polling.
- Prove rollout stability: a user never flickers between on/off as unrelated flags change.