Module 8: Project Ship Mercados Recommendations
The flag turned on and the ramp designed
Description
Module 1 ended with a decision: Mercado's team chooses to start small, with a 1% canary, instead of launching recommendations at 100% all at once. This lesson turns that decision into the real launch's first two concrete pieces: the flag that makes it possible to control exposure without a new deploy (module 2), and the full ramp of four stages that defines, in advance, how you climb from the canary to 100% (module 3). No new tool — isEnabled() and the ramp-design pattern from their source modules, applied in the order a real team would use them on launch day.
Connection to the module. This capstone doesn't introduce any new mechanism in this lesson: it reuses hashUserId() and isEnabled() exactly as they stood in module 2's lessons 4 and 5, and rolloutPlan()'s pattern exactly as it stood in module 3's lessons 2 and 4. What it adds is the correct sequence: first turn on the flag and verify it at the already-decided stage (1% canary), then design — in writing, with explicit criteria, before touching the flag again — the full ramp through to 100%. This lesson stops right before climbing to the 10% stage: watching that climb live is lesson 3's job, which follows this one.
An analogy: the switch already wired, and the itinerary already written
Think of two different preparations, both necessary, before a long night drive. The first is the car's light switch: it's already wired, it works, and you decide when to turn it on — you don't need to take apart the dashboard every time you want light. The second is the trip's itinerary: which city you stop to sleep in, how much gas you need between one stop and the next, and what signal would make you deviate from the plan — decided before starting the engine, not improvised at every intersection.
recommendations's flag is that switch: it's already wired (the code lives in production, turned off), and this lesson turns it on for the exact population module 1 decided to expose first. The four-stage ramp is the itinerary: the stops (1% → 10% → 50% → 100%), how many people are needed at each one to trust the measurement, and how long to wait before the next one — all written before the car leaves the canary.
Worked example: turn on the flag, design the ramp
// M8 L02: turns on recommendations's flag at the canary stage (1%, decided in
// module 1) and designs the complete 4-stage ramp BEFORE climbing beyond the
// canary. hashUserId() and isEnabled() are EXACTLY module 2's (L4, L5).
// rolloutPlan() follows module 3's same pattern (L2, L4).
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;
}
function rolloutPlan(stages) {
const results = [];
let halted = false;
for (const s of stages) {
if (halted) { results.push({ ...s, decision: 'NOT_REACHED' }); continue; }
const decision = s.advanceIf(s.measured) ? 'ADVANCE' : 'HOLD';
results.push({ ...s, decision });
if (decision === 'HOLD') halted = true;
}
return results;
}
console.log('=== Part 1: recommendations flag at canary (1%) ===');
const recommendationsFlag = { name: 'recommendations', enabled: true, rolloutPercent: 1 };
const sampleN = 5000;
const buyerIds = [];
for (let i = 0; i < sampleN; i++) buyerIds.push('buyer-' + String(i).padStart(5, '0'));
const enabledBuyers = buyerIds.filter((id) => isEnabled(id, recommendationsFlag));
console.log(enabledBuyers.length + ' of ' + sampleN + ' buyers see recommendations (' + (enabledBuyers.length / sampleN * 100).toFixed(2) + '%)');
console.log('\n=== Part 2: ramp designed (4 stages), before climbing past the canary ===');
const ramp = [
{ percent: 0.01, label: 'canary 1%', minSampleSize: 1000, minDwellHours: 4 },
{ percent: 0.10, label: 'rollout 10%', minSampleSize: 5000, minDwellHours: 12 },
{ percent: 0.50, label: 'rollout 50%', minSampleSize: 20000, minDwellHours: 24 },
{ percent: 1.00, label: 'rollout 100%', minSampleSize: 50000, minDwellHours: 24 },
];
ramp.forEach((s) => console.log(s.label.padEnd(14) + 'minSampleSize=' + String(s.minSampleSize).padStart(6) + ' minDwellHours=' + s.minDwellHours));
console.log('\n=== Part 3: rolloutPlan on the already-completed stage (canary) ===');
const ceiling = 800;
const canaryStage = [{ percent: 0.01, label: 'canary 1%', measured: { p95Latency: 720 }, advanceIf: (m) => m.p95Latency <= ceiling }];
const canaryResult = rolloutPlan(canaryStage);
canaryResult.forEach((s) => console.log(s.label + ': p95=' + s.measured.p95Latency + 'ms -> ' + s.decision));
console.log('\nCanary advances cleanly. The 10% stage gets watched LIVE in the next lesson.');
What to expect. Running the file with Node, the output is exactly this:
=== Part 1: recommendations flag at canary (1%) ===
50 of 5000 buyers see recommendations (1.00%)
=== Part 2: ramp designed (4 stages), before climbing past the canary ===
canary 1% minSampleSize= 1000 minDwellHours=4
rollout 10% minSampleSize= 5000 minDwellHours=12
rollout 50% minSampleSize= 20000 minDwellHours=24
rollout 100% minSampleSize= 50000 minDwellHours=24
=== Part 3: rolloutPlan on the already-completed stage (canary) ===
canary 1%: p95=720ms -> ADVANCE
Canary advances cleanly. The 10% stage gets watched LIVE in the next lesson.
Go over the three parts in order. Part 1 confirms the mechanism: with rolloutPercent: 1, exactly 1.00% of a 5,000-buyer sample ends up exposed — the same deterministic hashUserId() as always, no Math.random(), so anyone who runs this exact code gets the exact same result. Part 2 is this lesson's new piece: the full ramp table, with its percentage, its minimum sample size, and its minimum wait time per stage — decided before the flag climbs a single percentage point beyond the canary. Part 3 confirms that ramp's first stage, with the real data measured at canary (720ms, within the 800ms ceiling), advances cleanly.
Notice something important about Part 3: rolloutPlan() runs only on the canaryStage array, which has a single stage. That isn't an oversight — it's the correct discipline at this exact point in the launch: there's still no real data measured for 10%, 50%, or 100%, so there's no point evaluating them yet. Evaluating only what's actually been measured, and nothing more, is exactly the mistake module 3's project exercise 1 named: leaving a field as null and trusting the code "won't advance by mistake" is a real risk, not a hypothesis.
Why the order matters: flag first, then the full ramp
You might wonder why this lesson doesn't design the full ramp first and turn on the flag afterward — after all, both things happen "before really launching." The reason has to do with what information each step needs. Turning on the flag at the canary stage doesn't require having decided the 50% and 100% stages' criteria yet — it only requires the mechanism (isEnabled()) and the percentage module 1 already decided. Designing the full ramp, on the other hand, does benefit from having the canary already running: the following stages' minSampleSize and minDwellHours aren't arbitrary numbers — they reflect how much confidence the team needs before exposing more people, and that confidence is better calibrated with real canary data already in motion, not just with pre-launch intuition.
This doesn't mean the ramp gets designed "on the fly" — quite the opposite: the four criteria in Part 2 are fixed before the 10% stage starts, exactly as module 3's project's common mistake warned ("designing Part 1 after running Part 2, to justify a result you already saw"). The correct order is: canary running with real data → full ramp designed with that data as context, but before the next stage runs → each following stage evaluated against that already-fixed design, never renegotiated after seeing the result.
Common mistakes
Turning on the flag straight to the next stage's percentage (10%), skipping the verified canary. What happens: someone, with the full ramp already designed, decides to "save time" and bumps rolloutPercent straight to 10 without having first confirmed the 1% canary behaves as expected. Why it happens: the full ramp is already written, with all four stages visible, and skipping the first one feels like a reasonable optimization when "we're going to get there anyway." How to spot it: if at any point the flag's rolloutPercent jumps from 1 to 10 with no record of what was measured at 1% before the jump, the canary stage wasn't respected. How to fix it: as in this lesson's Part 3, every ramp stage gets evaluated with rolloutPlan() on real data from that specific stage before advancing to the next — never skipping a whole stage because the next one "was already planned."
Designing minSampleSize and minDwellHours the same for all four stages, instead of growing. What happens: someone copies the same minSampleSize: 1000 and minDwellHours: 4 for all four rows of Part 2's table, without adjusting the values to each stage's real size. Why it happens: writing one value and repeating it is faster than calculating, stage by stage, how many people and how much time are needed for a reliable measurement at that scale. How to spot it: if rollout 100%'s minSampleSize (which exposes 250,000 buyers) is the same as canary 1%'s (which exposes 2,500), the table doesn't reflect that a 1,000-person sample gives far less confidence over 250,000 buyers than over 2,500. How to fix it: as in this lesson's table, minSampleSize and minDwellHours grow with each stage — 1,000 → 5,000 → 20,000 → 50,000 — because the confidence you need before exposing more people scales with that stage's risk, it doesn't stay fixed.
Confusing "the flag is verified" with "the full ramp is already safe." What happens: after seeing the flag works correctly in Part 1 (1.00% exposure, stable), someone concludes the full launch is ready, without having designed the following stages' criteria yet. Why it happens: Part 1 gives a clear, satisfying technical confirmation, and that clarity can, mistakenly, feel like this lesson's complete work. How to spot it: if someone asks "and what happens if latency breaks at the 10% stage?" and there's no already-written criteria table to answer with, the ramp still isn't designed, even if the flag does work. How to fix it: the verified flag (Part 1) is a necessary condition, not a sufficient one — the full ramp (Part 2), with its explicit per-stage criteria, is the missing piece that turns "the mechanism works" into "the launch is planned."
Exercises
Exercise 1 — Change the canary percentage. Mercado's logistics team wants to apply this lesson's same pattern to its estimated-delivery-time algorithm (deliveryEtaV2), but starting with a 0.5% canary instead of 1%. Mentally run (or run in Node) Part 1 with rolloutPercent: 0.5 on the same 5,000-user sample. What percentage would you expect to see exposed, roughly?
See solution
With rolloutPercent: 0.5, isEnabled()'s condition bucket < flag.rolloutPercent is only true for users whose bucket (an integer from 0 to 99) is exactly 0 — that is, roughly 0.5% of the 5,000 users in the sample, about 25 buyers. Unlike a whole-number percentage like 1 or 10, a fractional percentage like 0.5 reduces how many bucket values qualify, but isEnabled()'s mechanism doesn't change at all — it's still the same exact comparison, just with a lower threshold.
Exercise 2 — Design the ramp for a case with less margin. The logistics team sets, for deliveryEtaV2, a minSampleSize of 2,000 at the 10% stage (higher than the 5,000... wait, lower than recommendations's) because its total base is smaller (80,000 orders per week, against Mercado's 250,000 buyers). If you keep the same proportion recommendations used between each stage's minSampleSize and the total base size, what minSampleSize would fit deliveryEtaV2's 50% stage?
See solution
In recommendations, the 50% stage has minSampleSize: 20,000 over a 250,000 base — a proportion of 20000 / 250000 = 8% of the total base. Applying that same proportion to deliveryEtaV2 (base of 80,000): 80000 * 0.08 = 6,400. This exercise's point isn't that 8% is a universal rule — each team can calibrate its own proportion — but that keeping the same proportional logic across different cases is a reasonable way to transfer a ramp design to a context with a differently-sized user base, instead of copying the absolute numbers unadjusted.
Exercise 3 — Explain the sequence in writing. In 3-4 sentences, explain to someone who doesn't know this guide why Mercado's team turns on the flag at the canary stage first, and designs the following stages' full ramp afterward — instead of designing everything in advance, including the canary, in a single step.
See solution
One example answer: "We turn on the flag at the smallest, most controlled stage first (the canary, 1% of our buyers) because that doesn't require any additional decision — the percentage was already defined from the blast-radius analysis. We design the following stages more carefully, using the canary's first real data as context, because the criteria for how many people we need to measure and how long to wait at each stage depend on how reliable the measurements turn out to be at that scale — something better calibrated by looking at real data than just pre-launch intuition. Even so, no criterion gets adjusted after seeing a stage's results — it gets fixed before that specific stage happens."
Summary and next step
In this lesson you turned on recommendations's flag at the exact stage module 1 decided (canary 1%, 1.00% exposure confirmed across 5,000 buyers), designed the full four-stage ramp with its minimum-size and wait-time criteria, and confirmed with rolloutPlan() that the first stage — the only one with real data so far — advances cleanly (720ms, within the 800ms ceiling).
Before moving on you should be able to: explain why minSampleSize and minDwellHours grow with each stage instead of staying fixed; and run isEnabled() by hand to roughly calculate how many users from a given sample would end up exposed at a different rollout percentage.
Lesson 3 takes this exact same ramp and watches it live, with the metrics guide's full four guardrails — not just latency — over the two stages that do have real data: the canary you just confirmed, and the 10% stage that hasn't been measured yet.
Resources
- Pete Hodgson (with Martin Fowler), "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. The complete feature flags reference behind this lesson's Part 1 mechanism. In English.
- Google SRE Workbook, Chapter 16, "Canarying Releases" — sre.google/workbook/canarying-releases. The formal practice of designing a ramp of successive stages, each with its own advance criteria — exactly this lesson's Part 2 table. In English.