Módulo 7: Strategy To Roadmap
El filtro estratégico
Descripción
La lección anterior te dio la etiqueta —servesDimensions— pero se detuvo justo antes de la pregunta decisiva: ¿esa etiqueta, comparada con la estrategia real de Mercado, hace que la apuesta pertenezca al juego o no? Esta lección construye la regla completa: el filtro estratégico, strategicFilter, que decide si una apuesta pertenece a la estrategia antes de que a nadie le importe cuánto vale su riceScore. Es, literalmente, el control de seguridad de la analogía del módulo: pasa quien tiene un pase válido, sin importar qué tan organizado venga el resto.
Conexión con el módulo. Esta es la lección bisagra de todo el módulo. Las lecciones 2 (traducir a apuestas) y 4-7 (la trampa del RICE alto, la coherencia, decir que no, el puente con RICE) son, cada una, una faceta distinta de la misma función que construyes aquí. Domina esta lección y el resto del módulo se vuelve, en gran medida, aplicar el mismo instrumento desde ángulos distintos.
Una analogía cotidiana: el portero de la fiesta
Imagina una fiesta con lista de invitados. En la puerta hay un portero con exactamente una pregunta que hacer: ¿tu nombre está en la lista? No pregunta si vienes bien vestido, no pregunta si traes un regalo caro, no pregunta si eres simpático o si conoces a alguien adentro. Puede llegar la persona más elegante de la noche, con el mejor regalo, la mejor conversación —y si su nombre no está en la lista, el portero no la deja pasar—. Y puede llegar alguien con ropa sencilla y las manos vacías, y si su nombre sí está en la lista, entra sin ninguna pregunta más.
El portero no está siendo injusto ni arbitrario: está aplicando el único criterio que le corresponde aplicar en la puerta. Una vez adentro, sí importan otras cosas —quién baila mejor, quién cuenta la mejor historia—, pero esas son preguntas para después de la puerta, no en ella. El filtro estratégico es exactamente ese portero: en la puerta del roadmap, la única pregunta que hace es "¿esta apuesta pertenece a la estrategia?" — qué tan bien puntúa en RICE es una pregunta legítima, importante, y completamente distinta, que se hace después, solo entre quienes ya pasaron la puerta.
Ejemplo trabajado: el backlog completo de Mercado, ordenado por RICE
Corremos strategicFilter sobre el backlog completo del trimestre de Mercado: las cinco apuestas que ya conoces de product-thinking-for-engineers-guide (fasterCheckout, recommendations, sellerTools, reviews, improvedSearch), más una sexta apuesta que nunca apareció en esa guía —lowestPriceMatch, igualar el precio más bajo del mercado—, cada una con la dimensión estratégica que dice servir y su riceScore ya calculado.
function riceScore({ reach, impact, confidence, effort }) {
return (reach * impact * confidence) / effort;
}
// Modelo pedagogico: una apuesta "pertenece a la estrategia" si al menos una
// de las dimensiones que sirve esta en strategy.winOn (lo que la estrategia
// eligio para GANAR, modulos 3-4) Y ninguna esta en strategy.avoid (lo que
// la estrategia eligio explicitamente NO pelear, modulo 2/5). Servir una
// dimension neutral (ni winOn ni avoid) no alcanza para pertenecer: la
// coherencia estrategica exige REFORZAR el juego elegido, no solo no estorbarlo.
function strategicFilter(backlog, strategy) {
return backlog.map((bet) => {
const reinforces = bet.servesDimensions.filter((d) => strategy.winOn.includes(d));
const conflicts = bet.servesDimensions.filter((d) => strategy.avoid.includes(d));
return {
feature: bet.feature,
servesDimensions: bet.servesDimensions,
reinforces,
conflicts,
inStrategy: reinforces.length > 0 && conflicts.length === 0,
riceScore: Number(riceScore(bet.rice).toFixed(2)),
};
});
}
const mercadoStrategy = { winOn: ['curatedDiscovery', 'sellerTrust'], avoid: ['price'] };
// El backlog completo: las cinco apuestas de product-thinking + la apuesta
// trampa (lowestPriceMatch), cada una con la dimension que dice servir.
const backlog = [
{ feature: 'fasterCheckout', servesDimensions: ['convenience'], rice: { reach: 8000, impact: 2, confidence: 0.8, effort: 2 } },
{ feature: 'recommendations', servesDimensions: ['curatedDiscovery'], rice: { reach: 5000, impact: 1, confidence: 0.5, effort: 3 } },
{ feature: 'sellerTools', servesDimensions: ['sellerTrust'], rice: { reach: 1200, impact: 2, confidence: 0.8, effort: 2 } },
{ feature: 'reviews', servesDimensions: ['sellerTrust'], rice: { reach: 6000, impact: 0.5, confidence: 0.8, effort: 1 } },
{ feature: 'improvedSearch', servesDimensions: ['catalogBreadth'], rice: { reach: 9000, impact: 1, confidence: 0.5, effort: 3 } },
{ feature: 'lowestPriceMatch', servesDimensions: ['price'], rice: { reach: 9500, impact: 3, confidence: 0.8, effort: 3 } },
];
console.log('=== strategicFilter: el backlog completo de Mercado, ordenado por riceScore ===\n');
const filtered = strategicFilter(backlog, mercadoStrategy).sort((a, b) => b.riceScore - a.riceScore);
console.table(filtered);
const inStrategy = filtered.filter((b) => b.inStrategy);
const outOfStrategy = filtered.filter((b) => !b.inStrategy);
console.log('Pasan el filtro (compiten por prioridad con RICE): ' + inStrategy.map((b) => b.feature).join(', '));
console.log('Se caen (sin importar su riceScore): ' + outOfStrategy.map((b) => b.feature).join(', '));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== strategicFilter: el backlog completo de Mercado, ordenado por riceScore ===
┌─────────┬────────────────────┬────────────────────────┬────────────────────────┬─────────────┬────────────┬───────────┐
│ (index) │ feature │ servesDimensions │ reinforces │ conflicts │ inStrategy │ riceScore │
├─────────┼────────────────────┼────────────────────────┼────────────────────────┼─────────────┼────────────┼───────────┤
│ 0 │ 'lowestPriceMatch' │ [ 'price' ] │ [] │ [ 'price' ] │ false │ 7600 │
│ 1 │ 'fasterCheckout' │ [ 'convenience' ] │ [] │ [] │ false │ 6400 │
│ 2 │ 'reviews' │ [ 'sellerTrust' ] │ [ 'sellerTrust' ] │ [] │ true │ 2400 │
│ 3 │ 'improvedSearch' │ [ 'catalogBreadth' ] │ [] │ [] │ false │ 1500 │
│ 4 │ 'sellerTools' │ [ 'sellerTrust' ] │ [ 'sellerTrust' ] │ [] │ true │ 960 │
│ 5 │ 'recommendations' │ [ 'curatedDiscovery' ] │ [ 'curatedDiscovery' ] │ [] │ true │ 833.33 │
└─────────┴────────────────────┴────────────────────────┴────────────────────────┴─────────────┴────────────┴───────────┘
Pasan el filtro (compiten por prioridad con RICE): reviews, sellerTools, recommendations
Se caen (sin importar su riceScore): lowestPriceMatch, fasterCheckout, improvedSearch
Lee esta tabla ordenada de arriba hacia abajo, por riceScore, y vas a ver el argumento entero del módulo en seis filas. Las dos apuestas con el riceScore más alto de todo el backlog —lowestPriceMatch con 7600 y fasterCheckout con 6400— tienen inStrategy: false. No es un empate raro ni un caso límite: son, literalmente, el primer y el segundo lugar de la tabla, y ninguna de las dos pasa el portero. lowestPriceMatch se cae por la razón más clara posible —conflicts no está vacío, compite exactamente en price, la dimensión que la estrategia de Mercado decidió no pelear—. fasterCheckout se cae por una razón más sutil, y por eso más importante de entender: no compite en ninguna dimensión prohibida (conflicts: []), pero tampoco refuerza ninguna de las dos dimensiones donde Mercado eligió ganar (reinforces: []) — sirve convenience, una dimensión real, pero neutral para esta estrategia. Servir algo que no está prohibido no es lo mismo que pertenecer.
Las tres que sí pasan —reviews, sellerTools, recommendations— tienen, cada una, reinforces con al menos un elemento: las tres refuerzan curatedDiscovery o sellerTrust, las dos dimensiones exactas donde la estrategia de Mercado, desde el módulo 2, eligió invertir. Fíjate en que estas tres tienen, además, los tres riceScore más bajos de todo el backlog. Eso no es casualidad de este ejemplo — es, casi siempre, la forma que toma el problema real: las apuestas más obviamente rentables por RICE puro suelen ser las más genéricas, las que sirven a cualquier producto de e-commerce sin importar su estrategia particular, precisamente porque no están concentradas en ningún juego específico.
Profundización: por qué "neutral" no es suficiente para pertenecer
La decisión de diseño más importante de strategicFilter —y la más fácil de pasar por alto— está en esta línea: inStrategy: reinforces.length > 0 && conflicts.length === 0. Fíjate en que no dice conflicts.length === 0 solo. Si el filtro dejara pasar cualquier apuesta que simplemente no conflictúe con avoid, fasterCheckout habría pasado —no compite en precio, después de todo—, y el filtro entero perdería su fuerza: casi cualquier apuesta razonable de un backlog de e-commerce evita competir directamente en precio, así que un filtro que solo exige "no conflictuar" dejaría pasar casi todo, exactamente como un portero que deja entrar a cualquiera que no traiga una bomba, en vez de exigir que su nombre esté en la lista.
Exigir reinforces.length > 0 —que la apuesta refuerce activamente al menos una dimensión de winOn— es lo que convierte al filtro en un instrumento real de estrategia, no solo en un detector de amenazas obvias. La pregunta que el filtro hace no es "¿esto le hace daño a nuestra estrategia?" —una barra bajísima que casi cualquier apuesta razonable pasa— sino "¿esto construye activamente el juego específico que elegimos jugar?" — una barra mucho más exigente, y la única que de verdad merece el nombre de "estratégica". Esta es la razón exacta por la que fasterCheckout, con el segundo mejor RICE de todo el backlog, no pasa: no le hace ningún daño a Mercado construirlo, pero tampoco avanza el juego específico que Mercado, con esfuerzo, en cinco módulos completos, decidió jugar.
Errores comunes
Priorizar por RICE sin filtrar por estrategia primero. Qué pasa: el equipo toma el backlog completo, lo puntúa con RICE (como en product-thinking-for-engineers-guide, módulo 3), y empieza a construir de arriba hacia abajo sin preguntarse antes si cada apuesta pertenece a la estrategia vigente. Por qué pasa: RICE produce un número limpio y ordenable, y ordenar por un número se siente como haber tomado ya la decisión difícil — la pregunta de pertenencia, más cualitativa, se siente menos urgente que un ranking ya calculado. Cómo detectarlo: si tu equipo puede nombrar el riceScore de su próxima apuesta pero no puede nombrar, en una frase, qué dimensión de la estrategia refuerza, el filtro nunca corrió. Cómo corregirlo: como viste en el ejemplo de esta lección, corre strategicFilter antes de mirar el riceScore de cualquier apuesta — la apuesta "eficiente" que más te desvía es, casi siempre, la que tiene el mejor número y la peor pertenencia.
Tratar "no conflictúa" como si fuera "pertenece". Qué pasa: alguien defiende una apuesta neutral —como fasterCheckout en el ejemplo de esta lección— con el argumento de que "no le hace daño a la estrategia", y la trata como si eso bastara para construirla con prioridad. Por qué pasa: es más fácil argumentar la ausencia de daño (una barra baja) que la presencia de refuerzo activo (una barra más alta y más específica) — y "no hace daño" suena, en una conversación rápida, casi tan bueno como "ayuda". Cómo detectarlo: pregunta explícitamente "¿qué dimensión de winOn refuerza esto?" — si la respuesta tarda en llegar o se conforma con "no compite con nada de lo que evitamos", la apuesta es neutral, no estratégica. Cómo corregirlo: exige siempre reinforces.length > 0, no solo conflicts.length === 0 — la línea exacta de código de esta lección que separa un filtro real de un simple detector de amenazas.
Aplicar el filtro con una estrategia mal definida y dejar que pase casi todo. Qué pasa: alguien construye un objeto strategy con un winOn demasiado amplio —por ejemplo, incluyendo cinco o seis dimensiones en vez de las dos o tres que de verdad definen la ventaja competitiva— y el filtro, aunque técnicamente correcto, deja de discriminar nada, porque casi cualquier apuesta refuerza alguna de las muchas dimensiones listadas. Por qué pasa: definir un winOn angosto se siente arriesgado —¿y si dejamos afuera algo importante?— así que la tentación es ampliarlo "por si acaso". Cómo detectarlo: si más del ochenta por ciento del backlog pasa el filtro, sospecha del winOn, no del backlog — un filtro que casi nunca filtra nada no está cumpliendo su función, sin importar cuán correcto esté el código. Cómo corregirlo: vuelve al posicionamiento del módulo 3 y a la diferenciación del módulo 4 — el winOn correcto es angosto a propósito, las mismas dos o tres dimensiones donde el producto de verdad concentra su ventaja, no una lista de todo lo que "estaría bien mejorar".
Ejercicios
Ejercicio 1 — Predice antes de correr. Sin ejecutar código, para una apuesta hipotética bulkOrdersForResellers con servesDimensions: ['catalogBreadth', 'sellerTrust'], ¿pasaría el filtro con la estrategia de Mercado (winOn: ['curatedDiscovery', 'sellerTrust'], avoid: ['price'])? Justifica con la regla exacta de inStrategy.
Ver solución
Sí pasaría. reinforces sería ['sellerTrust'] (la única de sus dos dimensiones que está en winOn) — con longitud mayor a cero, cumple la primera condición—. conflicts sería [], porque ninguna de sus dos dimensiones (catalogBreadth, sellerTrust) está en avoid (['price']) — cumple la segunda condición—. inStrategy sería true. El ejercicio muestra algo importante: una apuesta puede servir a varias dimensiones a la vez, algunas dentro de winOn y otras neutrales (como catalogBreadth aquí), y sigue pasando el filtro mientras al menos una refuerce y ninguna conflictúe — el filtro no exige que todas las dimensiones servidas sean estratégicas, solo que al menos una lo sea y ninguna esté prohibida.
Ejercicio 2 — Encuentra el caso límite. Una apuesta freeReturns tiene servesDimensions: ['sellerTrust', 'price'] — mejora la confianza del comprador, pero también implica absorber costos que presionan el precio hacia abajo. ¿Pasa el filtro? ¿Por qué el resultado podría sorprender a alguien que solo mira la primera dimensión de la lista?
Ver solución
No pasa. reinforces sería ['sellerTrust'] (longitud mayor a cero), pero conflicts sería ['price'] (también longitud mayor a cero, porque price está en avoid). La condición inStrategy exige conflicts.length === 0, así que el resultado final es false, sin importar que la apuesta también refuerce una dimensión ganadora. Esto sorprende a quien solo mira la primera dimensión de la lista y concluye "sirve sellerTrust, entonces pasa" — el filtro no promedia ni pondera entre dimensiones que refuerzan y dimensiones que conflictúan: basta un solo conflicto para caerse, sin importar cuántas dimensiones ganadoras también sirva. Es una regla estricta a propósito: mezclar una ventaja real con un compromiso en la dimensión prohibida no es una apuesta parcialmente buena, es una apuesta que diluye la estrategia por la puerta de atrás.
Ejercicio 3 — Explica el resultado a alguien que solo vio el riceScore. Un compañero de ingeniería, que solo revisó los números de RICE del backlog (sin ver strategicFilter), pregunta por qué el equipo no va a construir lowestPriceMatch primero, si tiene el riceScore más alto de todos. Escribe, en un párrafo, la respuesta.
Ver solución
Un ejemplo de respuesta: "Tienes razón en que lowestPriceMatch gana por goleada si solo miramos RICE — 7600 contra 6400 del segundo lugar. Pero RICE mide una sola cosa: valor esperado por unidad de costo, sin preguntar nunca si ese valor está alineado con el juego que elegimos jugar. Nuestra estrategia, desde el módulo 2, es explícita: ganamos con descubrimiento curado y confianza en el vendedor, no compitiendo en precio — de hecho, decidimos activamente NO pelear esa batalla, porque un jugador de la escala de un megastore genérico siempre nos va a ganar ahí. lowestPriceMatch es, literalmente, entrar a esa pelea que ya decidimos evitar. No importa cuán eficiente sea la apuesta en abstracto: no pertenece al juego que estamos jugando, y construirla primero solo porque el número es grande sería exactamente el tipo de decisión que ejecuta con precisión la estrategia equivocada."
Resumen y siguiente paso
Esta lección construyó el instrumento central del módulo: strategicFilter, que decide si una apuesta pertenece a la estrategia comparando las dimensiones que dice servir (servesDimensions) contra lo que la estrategia eligió para ganar (winOn) y lo que decidió no pelear (avoid). Sobre el backlog completo de Mercado, viste que las dos apuestas de mayor riceScore —lowestPriceMatch y fasterCheckout— se caen, una por conflicto directo y otra por ser meramente neutral, mientras que las tres apuestas con menor riceScore —reviews, sellerTools, recommendations— son, precisamente, las que refuerzan de verdad el juego elegido.
Antes de avanzar deberías poder: explicar de memoria por qué inStrategy exige reinforces.length > 0 y no solo conflicts.length === 0, y aplicar esa distinción para diagnosticar si una apuesta cualquiera pertenece o no a una estrategia dada.
La lección 4 se detiene, con más detalle todavía, en el patrón más peligroso que acabas de ver: qué hace que una apuesta de RICE alto y fuera de estrategia sea una trampa tan fácil de caer, y por qué el numerador grande de la fórmula es exactamente lo que la hace convincente.
Recursos
- Roger Martin, "Decoding the Strategy Choice Cascade" — rogermartin.medium.com/decoding-the-strategy-choice-cascade-475d40555eb1. La cascada de Martin funciona, en el fondo, como el mismo filtro de esta lección aplicado a cada nivel de decisión: cada elección debe pertenecer a la elección de arriba, o se descarta, sin importar cuán atractiva parezca en aislamiento. En inglés.
- Melissa Perri, Escaping the Build Trap — oreilly.com/library/view/escaping-the-build/9781491973767. El build trap, visto con este filtro: un equipo puede estar completamente ocupado construyendo apuestas de alto RICE y seguir cayendo en la trampa, si ninguna de esas apuestas pasa primero por la pregunta de pertenencia estratégica. En inglés.
- Marty Cagan (Silicon Valley Product Group), "Product Strategy" — svpg.com/product-strategy-overview. Cagan describe la estrategia de producto como el criterio que hace posible decir que no a ideas objetivamente buenas — la misma función que
strategicFilterautomatiza en código para el caso de Mercado. En inglés.