Module 4: Opportunity Sizing
Mini-project: size 3 bets from Mercado's backlog
Overview
It's time to bring the whole module together. You saw why "it looks important" isn't enough (L2), you opened up the formula piece by piece (L3), you learned to find the correct reachableUsers without falling into the trap of counting everyone (L4), to justify expectedLift with a source hierarchy instead of intuition (L5), to express uncertainty with honest ranges instead of false precision (L6), and to compare bets in the same unit so the one that "sounds boring" has the same chance of winning as the flashy one (L7). In this mini-project you use all of it on the three real bets from Mercado's backlog you've been seeing in fragments: faster checkout, recommendations at checkout, and seller reviews.
How this connects to the module. This project closes module 4 by sizing, with full ranges —not just the expected case—, the same three bets that showed up in lesson 1 (checkout), lesson 2 (recommendations), and the exercises from lessons 3, 5, and 6 (reviews), and compares them end to end with sizeRange and compareOpportunities together.
An analogy: the committee that distributes a limited budget
Imagine an apartment building committee that has to distribute an annual maintenance budget across several proposed improvements: fixing the elevator, painting the front, improving parking lot security. Each neighbor who proposes an improvement defends it with passion —"the elevator makes a horrible noise, we have to fix it now"—, but the committee can't allocate the budget based on who speaks loudest at the meeting. What it needs, for each proposal, is the same question answered the same way: how much does it cost, and how much real benefit does it bring, in the same unit —money, or measurable safety, not "felt urgency"—?
Sizing Mercado's backlog is that same committee, applied to an engineering quarter: three bets, each defensible in its own way, and the question the whole module has prepared you to answer — not "which feels most urgent?", but "which moves more GMV, with what confidence range, and under what assumptions?".
The reference solution, verified
Let's build the full sizing of the three bets and verify it, so you have a clear pattern. (The exercises at the end ask you to extend it and reason about it.)
Part 1 — The three bets, with their explicit assumptions and ranges
Each bet brings its two base assumptions (reachableUsers, baselineRate) measured in analytics, and an expectedLift range —pessimistic, expected, optimistic— following lessons 5 and 6's discipline: the expected comes from the best available source for each one, the pessimistic and optimistic are anchored to comparables, not made up.
// The 3 bets from Mercado's backlog for this quarter, each with its
// explicit assumptions (reachableUsers, baselineRate) and an expectedLift
// range (pessimistic / expected / optimistic), not a single number.
const backlog = [
{
name: 'faster checkout',
reachableUsers: 100000, // mobile checkout sessions/month (analytics)
baselineRate: 0.04, // mobile checkout conversion today (analytics)
pessimisticLift: 0.05, // the effect is barely noticeable
expectedLift: 0.15, // benchmark: last quarter's search speed test
optimisticLift: 0.30, // double the benchmark, with no more evidence than that
avgOrderValue: 45,
},
{
name: 'recommendations at checkout',
reachableUsers: 250000, // all checkout sessions, mobile+desktop/month
baselineRate: 0.55, // checkout completion rate today (analytics)
pessimisticLift: 0.01,
expectedLift: 0.03, // conservative estimate: reduces friction, does not eliminate it
optimisticLift: 0.06,
avgOrderValue: 45,
},
{
name: 'seller reviews',
reachableUsers: 40000, // purchases/month from a seller with no history with that buyer
baselineRate: 0.30, // completed checkout rate in that segment today (low, due to distrust)
pessimisticLift: 0.10,
expectedLift: 0.20, // team assumption: the trust effect of reviews
optimisticLift: 0.35,
avgOrderValue: 45,
},
];
Part 2 — The sizing run in Node
Now we run the whole module over the 3 bets: sizeRange for each one's honest range (L6), and compareOpportunities to order them in the same unit using the expected case (L7).
// PROJECT: size 3 bets from Mercado's backlog, with explicit assumptions
// and honest ranges, and compare them in the same unit.
function opportunitySize({ reachableUsers, baselineRate, expectedLift }) {
const baselineOutcome = Math.round(reachableUsers * baselineRate);
const newRate = baselineRate * (1 + expectedLift);
const newOutcome = Math.round(reachableUsers * newRate);
const extraOutcome = newOutcome - baselineOutcome;
return { baselineOutcome, newRate, newOutcome, extraOutcome };
}
function toGMV(extraOutcome, avgOrderValue) {
return Math.round(extraOutcome * avgOrderValue);
}
function sizeRange({ reachableUsers, baselineRate, pessimisticLift, expectedLift, optimisticLift, avgOrderValue }) {
const scenarios = { pessimistic: pessimisticLift, expected: expectedLift, optimistic: optimisticLift };
const result = {};
for (const [name, lift] of Object.entries(scenarios)) {
const { extraOutcome } = opportunitySize({ reachableUsers, baselineRate, expectedLift: lift });
result[name] = { extraOutcome, extraGMV: toGMV(extraOutcome, avgOrderValue) };
}
return result;
}
function compareOpportunities(bets) {
return bets
.map((bet) => {
const { extraOutcome } = opportunitySize(bet);
return { name: bet.name, extraOutcome, extraGMV: toGMV(extraOutcome, bet.avgOrderValue) };
})
.sort((a, b) => b.extraGMV - a.extraGMV);
}
// (the backlog from Part 1 goes here, unchanged)
console.log('=== Part 1: each bet, in its honest range ===\n');
for (const bet of backlog) {
console.log(`-- ${bet.name} --`);
const range = sizeRange(bet);
console.table(
Object.entries(range).map(([scenario, r]) => ({
scenario: scenario,
'extra GMV expected': r.extraGMV,
}))
);
}
console.log('=== Part 2: the 3 bets compared in the same unit (expected case) ===\n');
const ranking = compareOpportunities(backlog);
console.table(ranking.map(({ name, extraGMV }) => ({ bet: name, 'extra GMV/month': extraGMV })));
console.log(
`\nThe bet that sizes biggest is "${ranking[0].name}", with $${ranking[0].extraGMV.toLocaleString('en-US')} of extra GMV expected per month.`
);
What to expect. When you run the file with Node, the output is exactly this:
=== Part 1: each bet, in its honest range ===
-- faster checkout --
┌─────────┬───────────────┬────────────────────┐
│ (index) │ scenario │ extra GMV expected │
├─────────┼───────────────┼────────────────────┤
│ 0 │ 'pessimistic' │ 9000 │
│ 1 │ 'expected' │ 27000 │
│ 2 │ 'optimistic' │ 54000 │
└─────────┴───────────────┴────────────────────┘
-- recommendations at checkout --
┌─────────┬───────────────┬────────────────────┐
│ (index) │ scenario │ extra GMV expected │
├─────────┼───────────────┼────────────────────┤
│ 0 │ 'pessimistic' │ 61875 │
│ 1 │ 'expected' │ 185625 │
│ 2 │ 'optimistic' │ 371250 │
└─────────┴───────────────┴────────────────────┘
-- seller reviews --
┌─────────┬───────────────┬────────────────────┐
│ (index) │ scenario │ extra GMV expected │
├─────────┼───────────────┼────────────────────┤
│ 0 │ 'pessimistic' │ 54000 │
│ 1 │ 'expected' │ 108000 │
│ 2 │ 'optimistic' │ 189000 │
└─────────┴───────────────┴────────────────────┘
=== Part 2: the 3 bets compared in the same unit (expected case) ===
┌─────────┬───────────────────────────────┬─────────────────┐
│ (index) │ bet │ extra GMV/month │
├─────────┼───────────────────────────────┼─────────────────┤
│ 0 │ 'recommendations at checkout' │ 185625 │
│ 1 │ 'seller reviews' │ 108000 │
│ 2 │ 'faster checkout' │ 27000 │
└─────────┴───────────────────────────────┴─────────────────┘
The bet that sizes biggest is "recommendations at checkout", with $185,625 of extra GMV expected per month.
The result has two layers, and both matter. The first layer —the expected-case ranking— confirms what you already saw in lesson 7: "recommendations at checkout" sizes bigger than "seller reviews", which in turn sizes bigger than "faster checkout" — the opposite order a by-eye conversation about what "looks" more important would have predicted.
The second layer —the full ranges— adds something the expected-case ranking doesn't show on its own: even in its pessimistic scenario (61,875), "recommendations at checkout" already beats "faster checkout"'s optimistic scenario (54,000). That's not a close tie that depends on how well things go — it's a robust difference, one that holds even if "faster checkout"'s most optimistic assumption turned out true and "recommendations"'s most pessimistic one did too. Compare that to "seller reviews" (54,000 to 189,000) against "faster checkout" (9,000 to 54,000): there the ranges almost touch at the edge —checkout's optimistic ties reviews's pessimistic—, so that comparison is less robust and deserves more caution before dismissing "faster checkout" entirely in favor of "reviews".
What this project doesn't decide yet —and that's real information, not a flaw: the roadmap's final order. Sizing tells you how much each bet moves, not how much it costs to build, nor what its riskiest assumption is, nor how fast you need to decide before the cost of waiting gets high. With this project's three numbers in hand —27,000 / 108,000 / 185,625, with their ranges—, Mercado's team can already feed a new round of RICE's impact (module 3) with real data, instead of a 1-to-5 scale.
Common mistakes
Treating the expected-case ranking as the quarter's final decision. What happens: Part 2's table gets read and the conclusion is "let's build recommendations first, then reviews, and faster checkout last", with no look at effort, risk, or whether the ranges overlap. Why it happens: an ordered ranking looks like a decision already made, when it's actually an input for making one. How to spot it: if your quarter's plan is literally compareOpportunities's table order, with no adjustment, you're missing the rest of the conversation —effort (RICE), risk (module 6), cost of delay (module 7)—. How to fix it: use the ranking as prioritization's starting point, not its result — exactly the boundary this module's lesson 1 already marked.
Ignoring when ranges overlap and when they don't. What happens: "seller reviews beats faster checkout" gets treated with the same confidence as "recommendations beats faster checkout", without noticing the first comparison has ranges that almost touch and the second doesn't. Why it happens: looking only at the expected case is faster than comparing each bet's three scenarios against the other's. How to spot it: if you never checked whether one bet's optimistic crosses another's pessimistic, you don't know how robust your comparison is. How to fix it: for important decisions, compare the full ranges, not just the expected case — a difference that holds in the worst case against the rival's best case is far more defensible than one that only holds in both's expected case.
Accepting a sized bet without reviewing its original assumptions. What happens: Part 2's extraGMV gets taken and repeated in a presentation without checking again whether reachableUsers, baselineRate, and expectedLift are still reasonable —maybe the analytics data is already three months old, or the expectedLift benchmark was from a fairly different change—. Why it happens: once calculated, the number feels finished, and reviewing the assumptions again feels redundant. How to spot it: if you can't say, for each of the three bets, where each of its three assumptions came from (Part 1's code comments are exactly that traceability), you lost the trail. How to fix it: never present an extraGMV without its three assumptions alongside it — the same discipline from lesson 3, now applied to a full backlog, not an isolated bet.
Exercises
Exercise 1 — Add a fourth bet. Mercado's team proposes sizing "improved search" (spell correction and synonyms). With these assumptions, add it to the project's backlog and predict where in the ranking it would land: reachableUsers: 180000, baselineRate: 0.08, pessimisticLift: 0.03, expectedLift: 0.10, optimisticLift: 0.18, avgOrderValue: 45.
See solution
Expected case: baselineOutcome = 180000 × 0.08 = 14400; newRate = 0.08 × 1.10 = 0.088; newOutcome = 180000 × 0.088 = 15840; extraOutcome = 1440; extraGMV = 1440 × 45 = $64,800.
It would land third of four: recommendations (185,625) → seller reviews (108,000) → improved search (64,800) → faster checkout (27,000). As with the other three bets, it's worth reviewing the full range before locking in the order: pessimistic 180000 × 0.08 × 0.03 = 432 extra conversions → $19,440; optimistic 180000 × 0.08 × 0.18 = 2592 extra conversions → $116,640. Its range (19,440 to 116,640) overlaps with both "seller reviews" and "faster checkout" — unlike "recommendations", which dominated in every scenario, "improved search" is a less robust comparison and would deserve more caution before locking in its final position.
Exercise 2 — Filter the robust bets. Using the sizeRange results array for the project's 3 bets, write the logic (in prose or code) to identify which bets have a range that doesn't overlap with "faster checkout"'s (that is, their pessimistic scenario beats faster checkout's optimistic scenario, which is 54,000).
See solution
const fasterCheckoutOptimistic = 54000;
const robustAgainstCheckout = backlog
.filter((bet) => bet.name !== 'faster checkout')
.map((bet) => ({ name: bet.name, pessimisticGMV: sizeRange(bet).pessimistic.extraGMV }))
.filter((r) => r.pessimisticGMV > fasterCheckoutOptimistic);
Over the project's backlog, this would return only 'recommendations at checkout' (pessimistic: 61,875 > 54,000). "Seller reviews" is left out because its pessimistic (54,000) barely ties faster checkout's optimistic, doesn't beat it — the comparison between those two remains valid, but less conclusive than the recommendations one.
Exercise 3 — Defend the sizing in front of the team. Pick one of the project's three bets. Write, in 3-4 sentences, how you'd present it at the quarter's planning meeting: its expected extraGMV, its range, and where each of its three assumptions comes from. (There's no single correct answer — what's being evaluated is whether you use the module's vocabulary precisely and no number travels without its assumption.)
See solution
An example, for "seller reviews": "We sized this bet with three assumptions: 40,000 purchases a month from sellers with no history with that buyer —measured in the data warehouse—, a completed checkout rate of 30% in that segment today —also measured—, and an expected lift of 20% in that rate thanks to the trust effect of reviews, which is our least-backed assumption of the three, so we present it as a range: between 54,000 in the worst case and 189,000 in the best, with 108,000 as our central estimate. It's the second-biggest bet in this quarter's sized backlog, beating faster checkout even in its pessimistic scenario."
Notice the structure: the number never appears alone — always accompanied by its three assumptions, with each one's source, and presented as a range, not a single figure, exactly the module's full discipline.
Summary and next step
In this mini-project you sized Mercado's full backlog —three real bets for the quarter— with the whole module: opportunitySize for the base calculation, sizeRange for each one's honest uncertainty, and compareOpportunities to order them in the same unit. The result —recommendations first, with a robust margin even in its worst case; reviews second; faster checkout last— inverts the order a by-eye conversation would have predicted, and leaves you with three defensible numbers, each with its assumptions in plain sight, instead of three feelings.
With this you close module 4. You can now take any bet from your own backlog and answer, with a number and its explicit assumptions, "how much do we expect this to move?" — before writing a single line of code, and without faking a precision the estimate doesn't have.
Where you go next. You already know how much each bet sizes. The question that follows, for the bet you decide to build, is: what's the smallest version that actually tests this bet, without overbuilding?. That's module 5's question (module-05-mvp-and-scoping): the right MVP —not "v1 with fewer features", but the smallest thing that reduces uncertainty about the assumption you just sized—.
Resources
- Marty Cagan, Inspired — svpg.com/inspired-how-to-create-products-customers-love. The full book, now that you sized a real backlog, is worth rereading with that practice fresh. In English.
- Melissa Perri, Escaping the Build Trap — oreilly.com/library/view/escaping-the-build/9781491973767. The pattern of whole organizations building what "looks" important instead of what sizes big, now seen at the level of a full backlog. In English.
- Reforge, "Estimate business value for new features" — reforge.com/guides/estimate-business-value. How real product teams present a sized backlog, with ranges, to a decision committee — this project's same exercise, from industry. In English, requires a free account.
- Teresa Torres, Continuous Discovery Habits — producttalk.org/continuous-discovery-habits-book. The next step for the bet you decide to build: how to gain confidence that the
expectedLiftyou sized is true, by validating with real users. In English. - Itamar Gilad, "Product Discovery With ICE and The Confidence Meter" — itamargilad.com/the-tool-that-will-help-you-choose-better-product-ideas. To keep deepening how to calibrate the confidence behind each assumption in the sized backlog. In English.