Módulo 2: Feature Flags

Proyecto: pon las recomendaciones de Mercado detrás de un feature flag

Descripción

Las siete lecciones de este módulo construyeron, pieza por pieza, el mecanismo completo: qué distingue a un feature flag de un if común (L2), la anatomía de un flag real y su registro (L3), isEnabled(userId, flag) con hash determinista y rolloutPercent (L4), el kill switch (L5), los cuatro tipos de flag (L6), y cuándo un flag se convierte en deuda (L7). Este mini-proyecto te pide juntar las tres piezas técnicas centrales —el flag, la exposición estable, y el apagado de emergencia— en un solo ejercicio ejecutado, exactamente como lo haría el equipo de Mercado antes de encender recommendations para el primer comprador real.

Conexión con el módulo. Este proyecto no introduce ninguna función nueva: reutiliza hashUserId() e isEnabled() exactamente como quedaron en las lecciones 4 y 5, sin ningún cambio. Lo que agrega es la disciplina de verificación completa —definir, exponer, confirmar estabilidad, y probar el apagado— antes de considerar que un flag está listo para un lanzamiento real. El módulo 1 decidió cuánto exponer primero (canary al 1%, 125 de 250,000 compradores, dentro de un límite de 200); el módulo 3 va a diseñar la rampa completa que sube de esa etapa inicial hasta el 100% (1% → 10% → 50% → 100%, como ya adelantó el resumen del proyecto del módulo 1). Este proyecto verifica el flag en la segunda etapa de esa rampa —10%— precisamente porque a esa escala ya se puede confirmar el porcentaje con precisión sobre una muestra grande; el mecanismo que construyes hoy es el mismo, sin cambiar una sola línea, que va a sostener cada una de las cuatro etapas cuando el módulo 3 las diseñe.

Una analogía: el simulacro, antes de la alarma real

Ningún edificio con sentido común espera al primer incendio real para descubrir si la alarma contra incendios funciona. Antes de que haga falta de verdad, se hace un simulacro completo: se confirma que las luces de salida están donde deberían, que la puerta se abre desde adentro sin llave, y —la parte que más importa— se activa la alarma a propósito, una vez, para confirmar que suena y que todos saben qué hacer cuando suena. El simulacro no espera el incidente real para descubrir un problema; lo descubre con tiempo de sobra, cuando todavía no hay nada en riesgo.

Este proyecto es ese simulacro, aplicado al flag de recommendations. En vez de esperar al primer incidente real de latencia para descubrir si el kill switch funciona, lo activamos hoy, a propósito, sobre datos controlados —y confirmamos, con la misma seriedad que un simulacro de incendio, que apaga a todos, de verdad, al instante.

La decisión completa, paso a paso

Parte 1 — Definir el flag

El primer paso es el más simple, y el que sostiene todo lo demás: el objeto que representa recommendations en el registro de Mercado, con el rolloutPercent de la etapa que estamos verificando hoy.

Parte 2 — Exponer al 10% de forma estable, sobre una muestra representativa

Reutilizamos isEnabled() exactamente como quedó en la lección 4, corriéndolo sobre una muestra de 5,000 compradores (un proxy manejable de los 250,000 de la base real de Mercado) para confirmar el porcentaje con precisión.

Parte 3 — Verificar la estabilidad

Corremos el mismo rollout una segunda vez, como si fuera una request distinta en otro momento del día, y confirmamos que el conjunto exacto de usuarios expuestos no cambió ni un poco.

Parte 4 — Probar el kill switch

Simulamos el momento en que el guardrail de latencia se rompe y el equipo decide apagar recommendations — y confirmamos que la exposición cae a cero al instante, sin tocar rolloutPercent.

// PROYECTO: poner recommendations de Mercado detras de un feature flag.
// Definir el flag, exponer al 10% de forma deterministica y estable, y probar
// el kill switch (apagar -> 0% al instante, sin redeploy). Reutiliza
// hashUserId() e isEnabled() exactamente como quedaron en las lecciones 4 y 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;
}

// Parte 1: el flag
const recommendationsFlag = { name: 'recommendations', enabled: true, rolloutPercent: 10 };
console.log('=== Parte 1: el flag ===');
console.log(recommendationsFlag);

// Parte 2: exposicion al 10%, sobre una muestra de 5,000 compradores (proxy de
// los 250,000 de la base real de Mercado)
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=== Parte 2: exposicion sobre ' + sampleSize + ' compradores ===');
console.log(enabledRun1.length + ' de ' + sampleSize + ' ven recommendations = ' +
  (enabledRun1.length / sampleSize * 100).toFixed(2) + '%');

// Parte 3: estabilidad -- correr TODO el rollout una segunda vez, como si fuera
// una request distinta, y comparar que el conjunto de usuarios expuestos sea
// EXACTAMENTE el mismo.
const enabledRun2 = runRollout(recommendationsFlag);
const identical = enabledRun1.length === enabledRun2.length &&
  enabledRun1.every((id, i) => id === enabledRun2[i]);
console.log('\n=== Parte 3: estabilidad (misma corrida, dos veces) ===');
console.log('Run 1: ' + enabledRun1.length + ' usuarios expuestos');
console.log('Run 2: ' + enabledRun2.length + ' usuarios expuestos');
console.log('Son exactamente el mismo conjunto de usuarios: ' + identical);

// Parte 4: kill switch -- apagar el flag y confirmar 0% al instante, sin cambiar
// el codigo ni el rolloutPercent.
console.log('\n=== Parte 4: kill switch ===');
console.log('Antes del incidente: enabled=' + recommendationsFlag.enabled + ', ' + enabledRun1.length + ' usuarios expuestos.');
recommendationsFlag.enabled = false;
const enabledAfterKill = runRollout(recommendationsFlag);
console.log('Kill switch activado: enabled=' + recommendationsFlag.enabled + ' (rolloutPercent sigue en ' + recommendationsFlag.rolloutPercent + ', sin tocar)');
console.log('Usuarios expuestos ahora: ' + enabledAfterKill.length);

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== Parte 1: el flag ===
{ name: 'recommendations', enabled: true, rolloutPercent: 10 }

=== Parte 2: exposicion sobre 5000 compradores ===
497 de 5000 ven recommendations = 9.94%

=== Parte 3: estabilidad (misma corrida, dos veces) ===
Run 1: 497 usuarios expuestos
Run 2: 497 usuarios expuestos
Son exactamente el mismo conjunto de usuarios: true

=== Parte 4: kill switch ===
Antes del incidente: enabled=true, 497 usuarios expuestos.
Kill switch activado: enabled=false (rolloutPercent sigue en 10, sin tocar)
Usuarios expuestos ahora: 0

Repasa las cuatro partes con lo que cada una prueba. La Parte 1 confirma que el flag existe con la forma correcta —un rolloutPercent de 10, enabled: true—. La Parte 2 confirma el porcentaje: 9.94% de 5,000 compradores, prácticamente idéntico al 10% configurado —mucho más cerca del valor teórico que el 9.8% sobre 1,000 usuarios que viste en la lección 4, porque una muestra más grande reduce el margen de error—. La Parte 3 confirma la estabilidad: Run 1 y Run 2 no solo tienen la misma cantidad de usuarios expuestos (497 los dos), sino que identical: true confirma que son, literal, el mismo conjunto de personas — nadie entró ni salió entre las dos corridas. La Parte 4 confirma el kill switch: de 497 usuarios expuestos, la exposición cae a 0 con un solo cambio (enabled: false), sin que rolloutPercent se haya movido de 10.

La decisión, en limpio

VerificaciónResultado¿Pasa?
Porcentaje de exposición (objetivo: 10%)9.94% sobre 5,000 compradores
Estabilidad (mismo conjunto en dos corridas)497 = 497, conjunto idéntico
Kill switch (objetivo: 0% al instante)497 → 0, sin tocar rolloutPercent

Las tres verificaciones pasan, y las tres importan por razones distintas. Si el porcentaje hubiera salido muy lejos de 10% (digamos, 40%), el problema estaría en hashUserId() o en la comparación de isEnabled() — un bug en la lógica de asignación. Si la estabilidad hubiera fallado —un conjunto distinto entre Run 1 y Run 2—, el problema sería más grave todavía: alguna fuente de no-determinismo se coló en la función, exactamente el error que la lección 4 advirtió sobre Math.random(). Si el kill switch no hubiera llevado la exposición a cero, recommendations no tendría, de verdad, un mecanismo de emergencia — solo la ilusión de uno. Las tres juntas, pasando, son lo que le permite al equipo de Mercado decir, con evidencia y no con confianza ciega, que el flag está listo para sostener el rollout real que el módulo 3 va a diseñar.

Errores comunes

Confirmar el porcentaje sobre una muestra demasiado chica y confiar en el resultado. Qué pasa: alguien corre isEnabled() sobre los diez compradores nombrados de la lección 4 —donde salió exactamente 1 de 10— y da por confirmado el 10% sin correr una muestra más grande, sin notar que con diez personas, 0, 1 o 2 resultados hubieran sido igual de plausibles. Por qué pasa: correr sobre una lista chica es más rápido de leer y de razonar, y el resultado de la lección 4 (1 de 10, o sea 10% exacto) se sintió como confirmación suficiente. Cómo detectarlo: si la "confirmación" del porcentaje se basa en una muestra de menos de unos cientos de usuarios, el margen de error todavía es demasiado grande para confiar en el número. Cómo corregirlo: como en este proyecto, confirma el porcentaje sobre una muestra lo bastante grande (aquí, 5,000) para que el resultado (9.94%) esté claramente cerca del objetivo, no solo "en el rango posible".

Probar el kill switch en un ambiente de prueba, pero nunca en el mismo camino de código que usa producción. Qué pasa: el equipo prueba isEnabled() con enabled: false en un script separado, como el de este proyecto, pero nunca confirma que el código real de la página de producto de Mercado —el que de verdad llama a esta función en cada request— respeta el mismo resultado. Por qué pasa: un script de verificación aislado, como el de este proyecto, es más rápido de correr que probar el flujo completo de la aplicación, y es tentador dar por hecho que si la función funciona aislada, funciona igual integrada. Cómo detectarlo: si nadie ha probado el kill switch dentro del flujo real de la aplicación —no solo en un script suelto—, la verificación está incompleta. Cómo corregirlo: este proyecto verifica la lógica del mecanismo, que es el alcance de este módulo; verificarla integrada en el sistema real de Mercado es un paso adicional, necesario antes de un lanzamiento real, que se apoya en esta misma lógica pero la extiende más allá de lo que un ejercicio de Node puede cubrir.

Dar por cerrado el trabajo de este módulo sin haber decidido todavía el type del flag ni su plan de retiro. Qué pasa: el equipo termina las cuatro verificaciones de este proyecto, confirma que todo pasa, y considera que recommendations está completamente lista para lanzarse — sin haber usado todavía la clasificación de la lección 6 (type: 'release') ni haber anotado, como en la lección 7, cuándo debería revisarse si el flag ya cumplió su propósito. Por qué pasa: las cuatro verificaciones técnicas se sienten como "todo lo que hacía falta", porque son las que tienen números concretos y visibles. Cómo detectarlo: si nadie puede decir "este flag es de tipo X, y debería revisarse para retiro después de Y", el trabajo de las lecciones 6 y 7 todavía no se aplicó al caso real. Cómo corregirlo: el flag de recommendations de este proyecto es type: 'release' — eventualmente va a llegar a 100% y debería quitarse del código; anotar eso desde ahora, como parte de definir el flag y no como una idea posterior, es lo que evita que se convierta en la deuda que la lección 7 describió.

Ejercicios de transferencia

A diferencia de los ejercicios de las lecciones anteriores, este proyecto te pide aplicar el mecanismo completo a un caso que este módulo nunca vio — la prueba real de si aprendiste a construir y verificar un flag, o solo memoraste el resultado de recommendations.

Ejercicio 1 — Verifica un porcentaje distinto. El equipo de logística de Mercado quiere poner el algoritmo mejorado de tiempos de entrega (deliveryEtaV2, del ejercicio 1 de la lección 3) en un rollout al 25% en vez de 10%. Usando el mismo patrón de este proyecto (Partes 1 a 4), ¿qué cambiarías en el código, y qué porcentaje esperarías ver en la Parte 2 sobre una muestra de 5,000 usuarios?

Ver solución

Solo cambia un valor: const deliveryFlag = { name: 'deliveryEtaV2', enabled: true, rolloutPercent: 25 };, y en runRollout() se pasa deliveryFlag en vez de recommendationsFlag. Nada más del código necesita cambiar —ni hashUserId(), ni isEnabled(), ni la estructura de las cuatro partes—, porque el mecanismo es exactamente el mismo, solo con datos distintos. El resultado esperado en la Parte 2 sería un número cercano a 25% de 5,000 (alrededor de 1,250 compradores, con un margen de error similar al 0.06 puntos porcentuales que se observó con recommendations), y las Partes 3 y 4 deberían comportarse exactamente igual: mismo conjunto en dos corridas, y cero exposición después del kill switch.

Ejercicio 2 — Diseña la Parte 5. El equipo de Mercado quiere agregar una quinta verificación a este proyecto: confirmar que, al reactivar el flag después de haberlo apagado con el kill switch (enabled de false de vuelta a true), el conjunto exacto de usuarios expuestos vuelve a ser el mismo que antes del apagado —ni un usuario distinto—. Describe, en un par de líneas, cómo extenderías el código de este proyecto para verificar eso.

Ver solución
// Parte 5: reactivar y confirmar que el conjunto expuesto vuelve a ser el mismo
recommendationsFlag.enabled = true;
const enabledAfterReactivation = runRollout(recommendationsFlag);
const sameAsOriginal = enabledAfterReactivation.length === enabledRun1.length &&
  enabledAfterReactivation.every((id, i) => id === enabledRun1[i]);
console.log('\n=== Parte 5: reactivacion ===');
console.log('Usuarios expuestos tras reactivar: ' + enabledAfterReactivation.length);
console.log('Es el mismo conjunto que antes del kill switch: ' + sameAsOriginal);

Esta verificación debería dar true, y la razón es la misma que sostiene toda la estabilidad de isEnabled(): el bucket de cada usuario depende únicamente de userId + flag.name, nunca del historial de si el flag estuvo apagado o prendido antes. Apagar y volver a prender el flag no "revuelve" ni reasigna a nadie — cada usuario vuelve, exactamente, a la posición que ya tenía.

Ejercicio 3 — Reporta el resultado por escrito. Escribe el mensaje (100-150 palabras) que le enviarías al equipo de Mercado confirmando que recommendations está lista, técnicamente, para empezar su rollout real en el módulo 3. Incluye: el porcentaje verificado, la confirmación de estabilidad, la confirmación del kill switch, y qué sigue (sin explicar todavía la mecánica de la rampa completa, solo nombrando que existe).

Ver solución

Un mensaje posible: "El flag de recommendations está construido y verificado. Con rolloutPercent: 10, sobre una muestra de 5,000 compradores, el 9.94% quedó expuesto —dentro del margen esperado del 10% configurado—. Confirmamos estabilidad: corrimos la asignación dos veces, y el conjunto exacto de compradores expuestos fue idéntico las dos veces, así que ningún usuario va a ver la feature parpadear entre visitas. También probamos el kill switch: con un solo cambio de configuración, sin ningún deploy, la exposición cayó de 497 compradores a 0 al instante. El mecanismo está listo. Lo que falta ahora es diseñar la rampa completa —cuándo subir de la etapa actual a la siguiente, y con qué criterios—, que es el trabajo del próximo módulo." El mensaje nombra los tres resultados verificados con sus números exactos (no solo "funciona"), y deja explícito que la rampa completa —el cómo y cuándo subir el porcentaje— es un problema aparte, todavía sin resolver.

Resumen y siguiente paso

En este mini-proyecto verificaste el mecanismo completo del feature flag de recommendations: el flag definido con la forma correcta, una exposición del 9.94% sobre 5,000 compradores —prácticamente idéntica al 10% configurado—, un conjunto de usuarios expuestos idéntico entre dos corridas independientes, y una caída instantánea de 497 usuarios a 0 al activar el kill switch, sin tocar rolloutPercent. Con esto cierras el módulo 2: tienes, en código ejecutado y verificado, la herramienta que separa deploy de release —el interruptor que el módulo 1 nombró pero no construyó—.

Hacia dónde sigues. El módulo 3 toma este mismo mecanismo —el mismo isEnabled(), el mismo flag— y diseña la rampa completa del rollout: cómo se sube de la etapa de canary (1%, la que eligió el proyecto del módulo 1) a 10% (la que verificaste hoy), y de ahí a 50% y a 100%, con criterios explícitos para decidir cuándo avanzar de una etapa a la siguiente. El módulo 4 construye el dashboard que vigila el guardrail de latencia en cada una de esas etapas — la pieza de observabilidad que este módulo, centrado en el mecanismo del flag, no cubrió. Los módulos 5 a 7 completan el resto del ciclo: cuándo activar de verdad el kill switch que hoy solo probaste en simulacro, cómo escribir el postmortem, y cómo aplicar todo este marco a modelos de IA. El flag que construiste hoy es, literalmente, el interruptor que cada uno de esos módulos va a asumir que ya existe.

Recursos

  • Pete Hodgson (con Martin Fowler), "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. El artículo de referencia completo sobre feature flags, cuyos conceptos —categorías, implementación, deuda técnica— este proyecto ejecutó en versión simplificada. En inglés.
  • LaunchDarkly, "What Is Progressive Delivery All About?" — launchdarkly.com/blog/what-is-progressive-delivery-all-about. Cierra el módulo donde lo abrió la lección 1: cómo los feature flags habilitan exposición gradual y kill switches como práctica completa de la industria, la base de la rampa que el módulo 3 va a diseñar. En inglés.
  • Google SRE Workbook, Capítulo 16, "Canarying Releases" — sre.google/workbook/canarying-releases. La referencia formal sobre frameworks de feature flags que separan el lanzamiento de una feature de un release binario, exactamente el mecanismo que este proyecto verificó paso a paso. En inglés.