Module 2: Feature Flags
Project: put Mercado's recommendations behind a feature flag
Description
This module's seven lessons built, piece by piece, the complete mechanism: what distinguishes a feature flag from a plain if (L2), a real flag's anatomy and its registry (L3), isEnabled(userId, flag) with a deterministic hash and rolloutPercent (L4), the kill switch (L5), the four flag types (L6), and when a flag becomes debt (L7). This mini-project asks you to bring the three central technical pieces together — the flag, stable exposure, and the emergency shutdown — into a single executed exercise, exactly as Mercado's team would do before turning recommendations on for the first real buyer.
Connection to the module. This project introduces no new function: it reuses hashUserId() and isEnabled() exactly as they stood in lessons 4 and 5, with no changes. What it adds is the discipline of full verification — define, expose, confirm stability, and test the shutdown — before considering a flag ready for a real launch. Module 1 decided how much to expose first (1% canary, 125 of 250,000 buyers, within a limit of 200); module 3 is going to design the full ramp climbing from that initial stage all the way to 100% (1% → 10% → 50% → 100%, as module 1's project summary already previewed). This project verifies the flag at the second stage of that ramp — 10% — precisely because at that scale the percentage can already be confirmed with precision over a large sample; the mechanism you build today is the same one, without changing a single line, that's going to hold up each of the four stages when module 3 designs them.
An analogy: the drill, before the real alarm
No building with common sense waits for the first real fire to find out whether the fire alarm works. Before it's actually needed, a full drill is run: exit lights are confirmed to be where they should, the door is confirmed to open from the inside without a key, and — the part that matters most — the alarm is activated on purpose, once, to confirm it sounds and everyone knows what to do when it does. The drill doesn't wait for the real incident to discover a problem; it discovers it with plenty of time to spare, while nothing is actually at risk.
This project is that drill, applied to recommendations' flag. Instead of waiting for the first real latency incident to find out whether the kill switch works, we activate it today, on purpose, on controlled data — and confirm, with the same seriousness as a fire drill, that it really does turn everyone off, instantly.
The full decision, step by step
Part 1 — Define the flag
The first step is the simplest one, and the one everything else rests on: the object that represents recommendations in Mercado's registry, with the rolloutPercent of the stage we're verifying today.
Part 2 — Expose 10% stably, over a representative sample
We reuse isEnabled() exactly as it stood in lesson 4, running it over a sample of 5,000 buyers (a manageable proxy for Mercado's real 250,000-person base) to confirm the percentage with precision.
Part 3 — Verify stability
We run the same rollout a second time, as if it were a different request at another time of day, and confirm that the exact set of exposed users didn't change even slightly.
Part 4 — Test the kill switch
We simulate the moment the latency guardrail breaks and the team decides to turn recommendations off — and confirm that exposure drops to zero instantly, without touching rolloutPercent.
// PROJECT: put Mercado's recommendations behind a feature flag.
// Define the flag, expose 10% deterministically and stably, and test the kill
// switch (turn off -> 0% instantly, no redeploy). Reuses hashUserId() and
// isEnabled() exactly as they stood in lessons 4 and 5.
function hashUserId(userId) {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = (hash * 31 + userId.charCodeAt(i)) % 100;
}
return hash;
}
function isEnabled(userId, flag) {
if (!flag.enabled) return false;
const bucket = hashUserId(userId + flag.name);
return bucket < flag.rolloutPercent;
}
// Part 1: the flag
const recommendationsFlag = { name: 'recommendations', enabled: true, rolloutPercent: 10 };
console.log('=== Part 1: the flag ===');
console.log(recommendationsFlag);
// Part 2: 10% exposure, over a sample of 5,000 buyers (a proxy for Mercado's
// real base of 250,000)
const sampleSize = 5000;
const buyerIds = [];
for (let i = 0; i < sampleSize; i++) buyerIds.push('buyer-' + String(i).padStart(5, '0'));
function runRollout(flag) {
return buyerIds.filter((id) => isEnabled(id, flag));
}
const enabledRun1 = runRollout(recommendationsFlag);
console.log('\n=== Part 2: exposure over ' + sampleSize + ' buyers ===');
console.log(enabledRun1.length + ' of ' + sampleSize + ' see recommendations = ' +
(enabledRun1.length / sampleSize * 100).toFixed(2) + '%');
// Part 3: stability -- run the WHOLE rollout a second time, as if it were a
// different request, and compare that the set of exposed users is EXACTLY
// the same.
const enabledRun2 = runRollout(recommendationsFlag);
const identical = enabledRun1.length === enabledRun2.length &&
enabledRun1.every((id, i) => id === enabledRun2[i]);
console.log('\n=== Part 3: stability (same run, twice) ===');
console.log('Run 1: ' + enabledRun1.length + ' exposed users');
console.log('Run 2: ' + enabledRun2.length + ' exposed users');
console.log('Are they exactly the same set of users: ' + identical);
// Part 4: kill switch -- turn off the flag and confirm 0% instantly, without
// changing the code or rolloutPercent.
console.log('\n=== Part 4: kill switch ===');
console.log('Before the incident: enabled=' + recommendationsFlag.enabled + ', ' + enabledRun1.length + ' exposed users.');
recommendationsFlag.enabled = false;
const enabledAfterKill = runRollout(recommendationsFlag);
console.log('Kill switch activated: enabled=' + recommendationsFlag.enabled + ' (rolloutPercent stays at ' + recommendationsFlag.rolloutPercent + ', untouched)');
console.log('Exposed users now: ' + enabledAfterKill.length);
What to expect. Running the file with Node, the output is exactly this:
=== Part 1: the flag ===
{ name: 'recommendations', enabled: true, rolloutPercent: 10 }
=== Part 2: exposure over 5000 buyers ===
497 of 5000 see recommendations = 9.94%
=== Part 3: stability (same run, twice) ===
Run 1: 497 exposed users
Run 2: 497 exposed users
Are they exactly the same set of users: true
=== Part 4: kill switch ===
Before the incident: enabled=true, 497 exposed users.
Kill switch activated: enabled=false (rolloutPercent stays at 10, untouched)
Exposed users now: 0
Go over the four parts and what each one proves. Part 1 confirms the flag exists with the correct shape — a rolloutPercent of 10, enabled: true. Part 2 confirms the percentage: 9.94% of 5,000 buyers, practically identical to the configured 10% — much closer to the theoretical value than the 9.8% over 1,000 users you saw in lesson 4, because a bigger sample reduces the margin of error. Part 3 confirms stability: Run 1 and Run 2 not only have the same number of exposed users (497 both times), identical: true confirms they're, literally, the same set of people — nobody entered or left between the two runs. Part 4 confirms the kill switch: from 497 exposed users, exposure drops to 0 with a single change (enabled: false), with rolloutPercent never moving from 10.
The decision, laid out clearly
| Verification | Result | Passes? |
|---|---|---|
| Exposure percentage (target: 10%) | 9.94% over 5,000 buyers | Yes |
| Stability (same set across two runs) | 497 = 497, identical set | Yes |
| Kill switch (target: 0% instantly) | 497 → 0, without touching rolloutPercent | Yes |
All three verifications pass, and each matters for a different reason. If the percentage had come out far from 10% (say, 40%), the problem would be in hashUserId() or in isEnabled()'s comparison — a bug in the assignment logic. If stability had failed — a different set between Run 1 and Run 2 — the problem would be even worse: some source of non-determinism slipped into the function, exactly the mistake lesson 4 warned about with Math.random(). If the kill switch hadn't brought exposure to zero, recommendations wouldn't really have an emergency mechanism — only the illusion of one. All three together, passing, is what lets Mercado's team say, with evidence and not blind confidence, that the flag is ready to hold up the real rollout module 3 is going to design.
Common mistakes
Confirming the percentage on too small a sample and trusting the result. What happens: someone runs isEnabled() over lesson 4's ten named buyers — where exactly 1 of 10 came out — and considers the 10% confirmed without running a bigger sample, not noticing that with ten people, 0, 1, or 2 results would have been equally plausible. Why it happens: running over a small list is faster to read and reason about, and lesson 4's result (1 of 10, i.e., exactly 10%) felt like sufficient confirmation. How to spot it: if the percentage's "confirmation" rests on a sample of fewer than a few hundred users, the margin of error is still too big to trust the number. How to fix it: as in this project, confirm the percentage over a sample large enough (here, 5,000) that the result (9.94%) is clearly close to the target, not just "within the possible range."
Testing the kill switch in a test environment, but never on the same code path production uses. What happens: the team tests isEnabled() with enabled: false in a separate script, like this project's, but never confirms that Mercado's real product page code — the code that actually calls this function on every request — respects the same result. Why it happens: an isolated verification script, like this project's, is faster to run than testing the application's full flow, and it's tempting to assume that if the function works in isolation, it works the same integrated. How to spot it: if nobody has tested the kill switch inside the application's real flow — not just in a standalone script — the verification is incomplete. How to fix it: this project verifies the mechanism's logic, which is this module's scope; verifying it integrated into Mercado's real system is an additional step, necessary before a real launch, that builds on this same logic but extends it beyond what a Node exercise can cover.
Considering this module's work done without having decided the flag's type or its retirement plan yet. What happens: the team finishes this project's four verifications, confirms everything passes, and considers recommendations fully ready to launch — without having used lesson 6's classification (type: 'release') yet, or having noted, as in lesson 7, when it should be reviewed for whether the flag has already served its purpose. Why it happens: the four technical verifications feel like "everything that was needed," because they're the ones with concrete, visible numbers. How to spot it: if nobody can say "this flag is type X, and should be reviewed for retirement after Y," lessons 6 and 7's work hasn't been applied to the real case yet. How to fix it: this project's recommendations flag is type: 'release' — it's eventually going to reach 100% and should be removed from the code; noting that from the start, as part of defining the flag and not as an afterthought, is what keeps it from becoming the debt lesson 7 described.
Transfer exercises
Unlike the previous lessons' exercises, this project asks you to apply the full mechanism to a case this module never saw — the real test of whether you learned to build and verify a flag, or just memorized recommendations' result.
Exercise 1 — Verify a different percentage. Mercado's logistics team wants to put the improved delivery time algorithm (deliveryEtaV2, from lesson 3's exercise 1) into a 25% rollout instead of 10%. Using this project's same pattern (Parts 1 through 4), what would you change in the code, and what percentage would you expect to see in Part 2 over a sample of 5,000 users?
See solution
Only one value changes: const deliveryFlag = { name: 'deliveryEtaV2', enabled: true, rolloutPercent: 25 };, and runRollout() gets passed deliveryFlag instead of recommendationsFlag. Nothing else in the code needs to change — not hashUserId(), not isEnabled(), not the four-part structure — because the mechanism is exactly the same, just with different data. The expected result in Part 2 would be a number close to 25% of 5,000 (around 1,250 buyers, with a margin of error similar to the 0.06 percentage points observed with recommendations), and Parts 3 and 4 should behave exactly the same way: the same set across two runs, and zero exposure after the kill switch.
Exercise 2 — Design Part 5. Mercado's team wants to add a fifth verification to this project: confirming that, when reactivating the flag after turning it off with the kill switch (enabled going from false back to true), the exact set of exposed users goes back to being the same as before the shutdown — not a single different user. Describe, in a couple of lines, how you'd extend this project's code to verify that.
See solution
// Part 5: reactivate and confirm the exposed set goes back to being the same
recommendationsFlag.enabled = true;
const enabledAfterReactivation = runRollout(recommendationsFlag);
const sameAsOriginal = enabledAfterReactivation.length === enabledRun1.length &&
enabledAfterReactivation.every((id, i) => id === enabledRun1[i]);
console.log('\n=== Part 5: reactivation ===');
console.log('Exposed users after reactivating: ' + enabledAfterReactivation.length);
console.log('Is it the same set as before the kill switch: ' + sameAsOriginal);
This check should return true, and the reason is the same one holding up all of isEnabled()'s stability: each user's bucket depends only on userId + flag.name, never on the history of whether the flag was off or on before. Turning the flag off and back on doesn't "reshuffle" or reassign anyone — every user goes back, exactly, to the position they already had.
Exercise 3 — Report the result in writing. Write the message (100-150 words) you'd send to Mercado's team confirming that recommendations is technically ready to start its real rollout in module 3. Include: the verified percentage, the stability confirmation, the kill switch confirmation, and what comes next (without explaining the full ramp's mechanics yet, just naming that it exists).
See solution
A possible message: "recommendations' flag is built and verified. With rolloutPercent: 10, over a sample of 5,000 buyers, 9.94% ended up exposed — within the expected margin of the configured 10%. We confirmed stability: we ran the assignment twice, and the exact set of exposed buyers was identical both times, so no user is going to see the feature flicker between visits. We also tested the kill switch: with a single configuration change, no deploy at all, exposure dropped from 497 buyers to 0 instantly. The mechanism is ready. What's left now is designing the full ramp — when to climb from the current stage to the next one, and by what criteria — which is next module's work." The message names all three verified results with their exact numbers (not just "it works"), and makes explicit that the full ramp — how and when to raise the percentage — is a separate, still-unsolved problem.
Summary and next step
In this mini-project you verified recommendations' complete feature flag mechanism: the flag defined with the correct shape, 9.94% exposure over 5,000 buyers — practically identical to the configured 10% — an identical set of exposed users across two independent runs, and an instant drop from 497 users to 0 when activating the kill switch, with rolloutPercent untouched. With this you close module 2: you have, in run and verified code, the tool that separates deploy from release — the switch module 1 named but didn't build.
Where you go from here. Module 3 takes this same mechanism — the same isEnabled(), the same flag — and designs the full ramp of the rollout: how you climb from the canary stage (1%, the one module 1's project chose) to 10% (the one you verified today), and from there to 50% and to 100%, with explicit criteria for deciding when to advance from one stage to the next. Module 4 builds the dashboard that watches the latency guardrail at each of those stages — the observability piece this module, focused on the flag mechanism, didn't cover. Modules 5 through 7 complete the rest of the cycle: when to actually activate the kill switch you only drilled today, how to write the postmortem, and how to apply this whole framework to AI models. The flag you built today is, literally, the switch each of those modules is going to assume already exists.
Resources
- Pete Hodgson (with Martin Fowler), "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. The complete reference article on feature flags, whose concepts — categories, implementation, technical debt — this project ran in simplified form.
- LaunchDarkly, "What Is Progressive Delivery All About?" — launchdarkly.com/blog/what-is-progressive-delivery-all-about. Closes the module where lesson 1 opened it: how feature flags enable gradual exposure and kill switches as a full industry practice, the foundation for the ramp module 3 is going to design.
- Google SRE Workbook, Chapter 16, "Canarying Releases" — sre.google/workbook/canarying-releases. The formal reference on feature flag frameworks that separate a feature's rollout from a binary release, exactly the mechanism this project verified step by step.