Módulo 2: Feature Flags
Qué es (de verdad) un feature flag: anatomía y dónde vive
Descripción
La lección anterior demostró la propiedad que hace a un feature flag distinto de un if cualquiera: el valor que decide su comportamiento vive afuera del código. Pero "afuera" es todavía vago — ¿afuera dónde, exactamente, y con qué forma? Esta lección contesta las dos preguntas: la forma que toma un flag real (los campos que tiene, más allá del simple enabled que usaste en la lección 2) y el lugar donde varios flags conviven — un registro — de donde el código los lee en cada request.
Conexión con el módulo. Esta lección construye la estructura de datos exacta que las lecciones 4 y 5 van a extender: hoy el flag tiene name y enabled; la lección 4 le agrega rolloutPercent para exposición gradual; la lección 5 usa ese mismo enabled como kill switch. Nada de lo que sigue en este módulo tiene sentido sin la anatomía que ves hoy.
Una analogía: el interruptor con etiqueta, en el tablero eléctrico
Un interruptor de pared, solo, es útil para una habitación. Pero una casa completa no tiene un interruptor suelto por cuarto sin ningún orden — tiene un tablero eléctrico, donde cada circuito está etiquetado ("sala", "cocina", "luz exterior"), y cualquier persona que necesite intervenir —un electricista nuevo, un inquilino que nunca vivió ahí— puede abrir el tablero, leer las etiquetas, y entender de inmediato qué controla cada uno sin tener que rastrear cables por toda la casa.
Un feature flag real vive de la misma forma: no es un valor true/false suelto en algún rincón del código, es una entrada con nombre en un registro —el tablero— junto a otros flags, cada uno con su propia etiqueta y su propio estado. Cualquier ingeniero de Mercado, incluso uno que nunca tocó recommendations, puede abrir ese registro y entender, sin leer una sola línea de código de la feature, qué controla el flag, quién es responsable de él, y en qué estado está ahora mismo.
Ejemplo trabajado: el registro de flags de Mercado
Vamos a construir un registro con dos flags reales de Mercado, y una función que busca uno por nombre — exactamente como lo haría el código de producción en cada request:
// Registro de flags: donde viven de verdad (config/DB/servicio), no en el codigo.
const flagRegistry = [
{ name: 'recommendations', description: 'carrusel de productos recomendados en la pagina de producto', enabled: true, rolloutPercent: 10, owner: 'squad-discovery', type: 'release' },
{ name: 'checkoutVariantB', description: 'layout alterno del checkout, bajo prueba A/B', enabled: true, rolloutPercent: 50, owner: 'squad-checkout', type: 'experiment' },
];
function getFlag(name, registry) {
const found = registry.find((f) => f.name === name);
if (!found) throw new Error('flag no encontrado: ' + name);
return found;
}
console.log('=== Anatomia del flag "recommendations", leido del registro ===\n');
const rec = getFlag('recommendations', flagRegistry);
Object.entries(rec).forEach(([key, value]) => console.log(' ' + key.padEnd(16) + '= ' + value));
console.log('\n=== Cambiar rolloutPercent en el registro, sin tocar codigo ===');
console.log('Antes: rolloutPercent=' + rec.rolloutPercent);
rec.rolloutPercent = 25; // esto en produccion pasa en la UI de un servicio de flags o una fila de base de datos -- no en un editor de codigo
console.log('Despues: rolloutPercent=' + rec.rolloutPercent);
console.log('\nninguna funcion de la aplicacion se volvio a deployar entre estas dos lineas.');
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Anatomia del flag "recommendations", leido del registro ===
name = recommendations
description = carrusel de productos recomendados en la pagina de producto
enabled = true
rolloutPercent = 10
owner = squad-discovery
type = release
=== Cambiar rolloutPercent en el registro, sin tocar codigo ===
Antes: rolloutPercent=10
Despues: rolloutPercent=25
ninguna funcion de la aplicacion se volvio a deployar entre estas dos lineas.
Fíjate en los seis campos que tiene el flag, más allá del enabled binario de la lección anterior. name es el identificador único que el código usa para pedirlo (getFlag('recommendations', ...)) — tiene que ser estable, porque cambiar el nombre rompe cualquier lugar del código que lo esté leyendo. description existe para que alguien que nunca escribió la feature entienda, en una frase, qué controla — piensa en la etiqueta del tablero eléctrico. owner responde una pregunta que se vuelve crítica en un incidente real: si algo sale mal con recommendations a las tres de la mañana, ¿a qué equipo se despierta? type —que vas a desarrollar a fondo en la lección 6— dice si este flag es temporal o de largo plazo, y por lo tanto qué tan urgente es quitarlo después. Y rolloutPercent, el campo que la segunda mitad del ejemplo cambió en vivo, es exactamente la pieza que la lección 4 va a poner a trabajar de verdad.
Por qué el registro, y no el flag suelto
El ejemplo de la lección 2 tenía un solo flag, aislado. El de hoy tiene un registro: un arreglo (en producción, normalmente una tabla o un servicio) donde varios flags conviven, cada uno buscable por su name. Esa diferencia importa por una razón muy concreta: Mercado no tiene un solo feature flag — tiene, en cualquier momento dado, docenas, cada uno controlando una feature distinta, en distintas etapas de su ciclo de vida (recommendations apenas empezando su rollout al 10%; checkoutVariantB corriendo un experimento al 50%). Sin un registro central, cada flag terminaría viviendo en un lugar distinto del código, con su propia forma de leerse — exactamente el desorden que un tablero eléctrico sin orden produciría en una casa con veinte circuitos. El registro es lo que permite que getFlag('recommendations', flagRegistry) funcione igual que getFlag('checkoutVariantB', flagRegistry), sin que el código que los usa necesite saber nada especial sobre cada uno.
Vale la pena notar, para no adelantarse: el ejemplo de hoy sigue usando enabled como un valor binario simple — el registro define que existe un rolloutPercent, pero todavía no lo está usando para decidir, usuario por usuario, quién ve la feature. Esa lógica —la que convierte rolloutPercent: 10 en una decisión real para cada comprador— es exactamente el trabajo de la lección 4.
Errores comunes
Guardar el flag en una variable global del código, en vez de en un registro externo real. Qué pasa: alguien declara let recommendationsEnabled = true; en algún archivo del proyecto, y aunque técnicamente se puede cambiar sin tocar la lógica de negocio, cambiar esa variable sigue exigiendo editar el código fuente y deployar. Por qué pasa: una variable en el código se siente "separada" de la lógica que la usa, y es fácil confundir esa separación superficial con la separación real que exige un feature flag —vivir en un lugar que el código deployado puede leer sin recompilarse—. Cómo detectarlo: la pregunta de la lección 2 sigue siendo la prueba correcta — ¿puedo cambiar este valor sin ningún deploy? Si la respuesta involucra tocar un archivo .js o .py, no es un registro externo de verdad. Cómo corregirlo: como en el ejemplo de hoy, el registro vive afuera del archivo de la lógica de negocio — en producción, en una base de datos o un servicio dedicado que el código consulta, no en una declaración de variable dentro del mismo módulo.
Omitir owner y description porque "ya sabemos qué hace el flag". Qué pasa: el equipo que crea recommendations sabe exactamente qué controla y quién es responsable, así que no se molesta en llenar esos campos — hasta que, meses después, otro ingeniero (o el mismo, que ya lo olvidó) encuentra el flag en el registro y no tiene ninguna pista de qué hace ni a quién preguntarle. Por qué pasa: el contexto está fresco en la cabeza de quien crea el flag, y llenar metadatos se siente como trabajo extra sin beneficio inmediato. Cómo detectarlo: si alguien pregunta "¿qué hace este flag?" y la única forma de saberlo es leer el código que lo consume (en vez de leer el registro), los metadatos no cumplieron su propósito. Cómo corregirlo: trata description y owner como parte obligatoria de crear cualquier flag, no como documentación opcional — son, literalmente, lo que hace que el registro sea legible por alguien que no escribió la feature, exactamente como las etiquetas del tablero eléctrico.
Asumir que cambiar rolloutPercent en el registro tiene efecto inmediato, sin haber construido todavía la lógica que lo lee. Qué pasa: el equipo cambia rolloutPercent de 10 a 25 en el registro, como en el ejemplo de hoy, y espera que automáticamente el 25% de los usuarios empiece a ver recommendations — pero el campo, por sí solo, no hace nada; hace falta una función que lo lea y decida, usuario por usuario. Por qué pasa: el ejemplo de esta lección muestra que el dato cambia sin deploy, y es fácil confundir eso con que el dato actúa sin deploy. Cómo detectarlo: si cambiar rolloutPercent en el registro no mueve ningún número real de usuarios expuestos, falta la pieza de decisión. Cómo corregirlo: rolloutPercent es un dato — la lección 4 construye la función, isEnabled(userId, flag), que de verdad lo convierte en una decisión por usuario. Un campo en un objeto y la lógica que lo interpreta son dos cosas distintas, y las dos hacen falta.
Ejercicios
Ejercicio 1 — Agrega un flag al registro. El equipo de logística de Mercado necesita un flag nuevo, deliveryEtaV2, para un algoritmo mejorado de tiempos de entrega estimados, todavía sin activar para nadie, dueño del equipo squad-logistics, en etapa de rollout inicial. Escribe el objeto completo, con los seis campos del ejemplo de esta lección.
Ver solución
{
name: 'deliveryEtaV2',
description: 'algoritmo mejorado de tiempos de entrega estimados',
enabled: false,
rolloutPercent: 0,
owner: 'squad-logistics',
type: 'release',
}
Nota que enabled: false y rolloutPercent: 0 describen, en conjunto, "todavía sin activar para nadie" — no hace falta elegir entre uno u otro campo para expresar esto; los dos, juntos, dejan la intención sin ambigüedad. type: 'release' porque, como vas a ver en la lección 6, un algoritmo nuevo que eventualmente llega al 100% y se retira es exactamente el patrón de un flag de tipo release.
Ejercicio 2 — Encuentra el bug. Un compañero escribe esta función y se queja de que getFlag('CheckoutVariantB', flagRegistry) le lanza el error 'flag no encontrado: CheckoutVariantB', aunque el flag sí existe en el registro del ejemplo de esta lección. ¿Qué está pasando?
Ver solución
getFlag() usa f.name === name — una comparación exacta, sensible a mayúsculas y minúsculas. El flag en el registro se llama 'checkoutVariantB' (con "c" minúscula), y la búsqueda se hizo con 'CheckoutVariantB' (con "C" mayúscula) — como JavaScript distingue mayúsculas de minúsculas en las cadenas de texto, 'checkoutVariantB' === 'CheckoutVariantB' da false, y la función no encuentra ninguna coincidencia. La corrección no está en el código de getFlag() —está funcionando exactamente como debería—, sino en usar el name exacto tal como está guardado en el registro. Esto es, en la práctica, una razón más para que name sea un valor consistente y bien documentado, no algo que cada persona escribe de memoria.
Ejercicio 3 — Explica el registro sin usar la palabra "flag". En dos o tres frases, explica a alguien nuevo en el equipo qué es flagRegistry y para qué sirve getFlag(), sin usar la palabra "flag" ni "feature flag". Puedes usar la analogía del tablero eléctrico.
Ver solución
Un ejemplo de respuesta: "Es como el tablero eléctrico de una casa grande: en vez de tener cada interruptor suelto, escondido en un lugar distinto, hay un solo lugar donde están todos, cada uno con su etiqueta —qué controla, quién es responsable— y su posición actual. Cuando alguien necesita saber o cambiar el estado de algo específico, no tiene que buscar por toda la casa: va al tablero, busca la etiqueta correcta, y ahí está." La idea central, sin el vocabulario técnico: un lugar único, organizado y con nombre para cada interruptor, en vez de interruptores dispersos y sin documentar.
Resumen y siguiente paso
En esta lección construiste la anatomía completa de un feature flag real —name, description, enabled, rolloutPercent, owner, type— y viste cómo varios flags conviven en un registro central, buscable por nombre, exactamente como lo haría un sistema de producción. Confirmaste, ejecutando el ejemplo, que cambiar un campo del registro (rolloutPercent: 10 a 25) no requiere deployar ninguna función de la aplicación — la separación de la lección 2, ahora con una forma concreta.
Antes de avanzar deberías poder: nombrar los seis campos de un flag y explicar para qué sirve cada uno; explicar por qué los flags viven en un registro central y no dispersos por el código; y distinguir entre "el dato cambió" (el registro) y "el dato produjo un efecto" (la lógica que todavía falta construir).
La lección 4 construye exactamente esa lógica que falta: isEnabled(userId, flag), la función que lee rolloutPercent del registro y decide, para cada comprador de Mercado, si ve recommendations — con un hash determinista que garantiza que el mismo comprador siempre obtiene la misma respuesta.
Recursos
- Pete Hodgson (con Martin Fowler), "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. La sección sobre almacenar el estado de los toggles describe justamente por qué un feature flag necesita vivir en un lugar centralizado y consultable, más allá de un simple valor suelto. En inglés.
- Google SRE Workbook, Capítulo 16, "Canarying Releases" — sre.google/workbook/canarying-releases. Menciona explícitamente frameworks de feature flags/experimentos como el mecanismo que "permite separar el lanzamiento de una feature de un release binario" — la misma idea que este módulo desarrolla con código. En inglés.