Módulo 2: Feature Flags

La deuda de flags: cuándo (y cómo) quitarlos

Descripción

La lección 6 separó los flags de Mercado en dos grupos: los que tienen fecha de retiro natural (release, experiment) y los que no (operational, permanent). Esta lección cierra el arco temático del módulo con lo que pasa cuando un flag del primer grupo no se retira a tiempo. Un if (isEnabled('recommendations', ...)) que sigue en el código meses después de que el rollout llegó a 100% no es inofensivo — es deuda de flags: código que sigue existiendo, sigue siendo leído, sigue teniendo que entenderse, sin cumplir ya ningún propósito.

Conexión con el módulo. Esta es la última lección de tema antes del proyecto, y junta todo lo construido: el registro de la lección 3, el rolloutPercent de la lección 4, y la clasificación por type de la lección 6 son, juntos, exactamente los datos que hacen falta para detectar la deuda con un modelo ejecutado, no con una corazonada.

Una analogía: los cables sueltos detrás del clóset eléctrico

Vuelve al tablero eléctrico de la lección 3. Con los años, una casa acumula remodelaciones: un cuarto que se convirtió en dos, una instalación temporal para una fiesta que nunca se desconectó, un circuito de una lavadora que se cambió de lugar hace tres años. Si nadie retira lo que ya no se usa, el tablero termina lleno de interruptores etiquetados a mano con letra borrosa, algunos sin etiqueta, y nadie —ni el electricista original, mucho menos uno nuevo— puede decir con certeza cuáles son seguros de quitar y cuáles todavía alimentan algo real.

Esa es, exactamente, la deuda de flags: no es que un flag viejo rompa nada activamente —igual que un cable suelto detrás del clóset no necesariamente causa un cortocircuito—, es que su sola presencia hace que cada persona que después necesite entender o modificar el sistema tenga que decidir, sin información clara, si es seguro tocarlo. Cuantos más flags viejos se acumulan, más lento y más arriesgado se vuelve cualquier cambio futuro, incluso uno que no tiene nada que ver con esos flags.

Ejemplo trabajado: flagDebtReport() sobre el registro de Mercado

Vamos a construir un modelo que revisa cuatro flags reales de Mercado —incluyendo su type y cuántos días llevan en su estado actual— y decide cuáles son candidatos a deuda:

function flagDebtReport(flags) {
  return flags.map((f) => {
    let isDebt = false;
    let reason = 'en uso activo, sin senales de deuda';
    if (f.type === 'release' && f.rolloutPercent === 100 && f.daysSinceCreated > 30) {
      isDebt = true;
      reason = 'release al 100% desde hace ' + f.daysSinceCreated + ' dias -- el rollout termino, el flag deberia quitarse del codigo';
    } else if (f.type === 'experiment' && f.experimentClosed) {
      isDebt = true;
      reason = 'el experimento ya cerro (guia de metricas) pero el flag sigue en el codigo';
    } else if (f.type === 'operational' || f.type === 'permanent') {
      reason = f.type + ' -- se espera que viva de forma indefinida, no es deuda';
    }
    return { ...f, isDebt, reason };
  });
}

const flagsWithAge = [
  { name: 'newCheckoutLayout', type: 'release', rolloutPercent: 100, daysSinceCreated: 96 },
  { name: 'recommendations', type: 'release', rolloutPercent: 10, daysSinceCreated: 4 },
  { name: 'checkoutVariantB', type: 'experiment', rolloutPercent: 50, daysSinceCreated: 61, experimentClosed: true },
  { name: 'disableSellerPayouts', type: 'operational', rolloutPercent: 100, daysSinceCreated: 210 },
];

console.log('=== flagDebtReport sobre los flags de Mercado ===\n');
const report = flagDebtReport(flagsWithAge);
report.forEach((f) => {
  console.log(f.name.padEnd(22) + 'isDebt=' + f.isDebt);
  console.log('  -> ' + f.reason + '\n');
});

const debtCount = report.filter((f) => f.isDebt).length;
console.log('Total: ' + debtCount + ' de ' + report.length + ' flags son candidatos a deuda.');

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

=== flagDebtReport sobre los flags de Mercado ===

newCheckoutLayout     isDebt=true
  -> release al 100% desde hace 96 dias -- el rollout termino, el flag deberia quitarse del codigo

recommendations       isDebt=false
  -> en uso activo, sin senales de deuda

checkoutVariantB      isDebt=true
  -> el experimento ya cerro (guia de metricas) pero el flag sigue en el codigo

disableSellerPayouts  isDebt=false
  -> operational -- se espera que viva de forma indefinida, no es deuda

Total: 2 de 4 flags son candidatos a deuda.

Fíjate en los cuatro veredictos, uno por uno. newCheckoutLayout es deuda clara: es un flag release, llegó a rolloutPercent: 100 hace 96 días, y nadie lo quitó — el rollout terminó hace más de tres meses, y el código todavía carga un if que siempre evalúa lo mismo. recommendations, en cambio, no es deuda: apenas lleva 4 días en un rollout todavía en progreso al 10% — está haciendo exactamente el trabajo para el que se creó. checkoutVariantB es deuda por una razón distinta: no importa cuántos días lleve —61 en este caso—, lo que lo marca es que experimentClosed: true — el experimento ya terminó, la guía de métricas ya tiene su resultado, y el flag sigue en el código sin que nadie lo haya limpiado. disableSellerPayouts no es deuda, y la razón es exactamente la de la lección 6: es operational, se espera que viva indefinidamente, así que ni siquiera se evalúa contra un umbral de días.

Por qué el criterio depende del type, no solo de la edad

Nota algo importante en el modelo: flagDebtReport() nunca pregunta "¿cuántos días lleva este flag existiendo?" de forma aislada — siempre lo pregunta junto con el type. Un flag release de 96 días es sospechoso porque los flags release están diseñados para vivir semanas, no meses. Un flag operational de 210 días —más viejo que cualquiera de los otros tres— no genera ninguna alerta, porque esa es exactamente su vida esperada. Si el modelo solo mirara la edad, sin el type, marcaría a disableSellerPayouts como el peor caso de deuda de los cuatro —el más viejo—, cuando en realidad es el único diseñado, a propósito, para durar tanto tiempo. Ese es el motivo exacto por el que la lección 6 tuvo que llegar primero: sin la clasificación por tipo, cualquier intento de medir deuda de flags termina penalizando a los flags que están funcionando exactamente como deberían.

Vale la pena notar también el 30 que aparece como umbral para los flags release — es un número pedagógico para este ejemplo, no una regla universal. Cada equipo, y cada organización, define su propio umbral razonable según qué tan rápido espera que un rollout llegue a 100% y se confirme estable; lo que no cambia entre organizaciones es la idea de fondo: un flag temporal necesita algún umbral de edad contra el que compararse, y uno permanente no.

Errores comunes

Nunca fijar un umbral, y confiar en que "alguien se va a acordar de quitarlo". Qué pasa: el equipo crea flags release sin ningún proceso que revise, periódicamente, cuáles llegaron a 100% hace tiempo — confiando en la memoria individual de quien lo creó, que suele estar trabajando en otra cosa para cuando el rollout termina. Por qué pasa: quitar un flag no tiene la urgencia de crear uno —nada se rompe visiblemente si el flag viejo se queda—, así que compite, y pierde, contra cualquier tarea con una fecha límite real. Cómo detectarlo: si nadie puede decir cuántos flags release en el registro de Mercado están al 100% desde hace más de un mes sin haber sido revisados, no existe ningún proceso — solo buenas intenciones. Cómo corregirlo: un reporte como flagDebtReport(), corrido regularmente (no una sola vez), convierte "alguien se va a acordar" en una lista concreta y verificable de candidatos, exactamente como el reporte de esta lección.

Borrar un flag de deuda sin verificar que de verdad ya no se usa en ninguna parte. Qué pasa: alguien ve newCheckoutLayout marcado como deuda y borra el flag del registro de inmediato, sin revisar si todavía existe código en producción que llama isEnabled('newCheckoutLayout', ...) — y ese código, al no encontrar el flag, puede fallar de formas inesperadas según cómo esté escrito el manejo de errores. Por qué pasa: el reporte de deuda identifica candidatos con buena confianza, pero "candidato a revisar" y "seguro de borrar sin más pasos" no son lo mismo — quitar un flag de verdad tiene dos partes: quitarlo del registro, y quitar del código el if que lo consultaba, en ese orden. Cómo detectarlo: si borrar un flag del registro causa errores en producción, faltó el segundo paso. Cómo corregirlo: tratar un flag isDebt: true como el inicio de un trabajo de limpieza —revisar y quitar el código que lo consulta primero, y solo después retirarlo del registro—, no como una acción de un solo paso.

Medir deuda de flags solo por cantidad total, sin distinguir tipos. Qué pasa: un equipo se fija la meta de "tener menos de 20 flags en el registro" y, para cumplirla, presiona por igual para reducir flags release viejos y flags operational que llevan años funcionando correctamente — tratando la cantidad total como si toda ella fuera deuda por igual. Por qué pasa: un número total es más fácil de comunicar en una meta de equipo que una distinción de cuatro categorías, y la presión por simplificar termina aplanando una diferencia que sí importa. Cómo detectarlo: si la meta de "reducir flags" no distingue entre tipos, alguien eventualmente va a proponer borrar un kill switch operacional solo para bajar el número total. Cómo corregirlo: como en el modelo de esta lección, la métrica correcta no es "cuántos flags hay", es "cuántos flags temporales (release, experiment) superan su umbral de edad esperado" — un número mucho más chico, y mucho más accionable, que el total.

Ejercicios

Ejercicio 1 — Agrega un quinto flag. Mercado tiene un flag release llamado newSearchRanking, con rolloutPercent: 100 y daysSinceCreated: 12. Usando el criterio de flagDebtReport() (umbral de 30 días para release), ¿es deuda? Justifica con el mismo formato de reason que usa el modelo.

Ver solución

No es deuda: aunque rolloutPercent: 100 cumple la primera condición, daysSinceCreated: 12 no supera el umbral de 30 días —la condición completa exige f.rolloutPercent === 100 && f.daysSinceCreated > 30, y 12 > 30 es falso—. El reason sería 'en uso activo, sin senales de deuda', el mismo que obtuvo recommendations en el ejemplo. Esto ilustra un punto importante: llegar a 100% no marca deuda de inmediato — el equipo necesita un margen razonable (aquí, hasta 30 días) para confirmar que el rollout completo es estable antes de que se espere que alguien quite el flag del código.

Ejercicio 2 — Encuentra el caso ambiguo. Un flag experiment tiene daysSinceCreated: 200 pero experimentClosed: false (el experimento sigue corriendo activamente, con resultados todavía no significativos). Según el modelo de esta lección, ¿es deuda? ¿Te parece que debería serlo, aunque el modelo diga que no?

Ver solución

Según el modelo exacto de esta lección, no es deuda —la condición para experiment solo revisa experimentClosed, no la edad—, así que el reason sería 'en uso activo, sin senales de deuda'. Pero 200 días es un tiempo inusualmente largo para que un experimento siga sin significancia (la guía de métricas de este ecosistema trata la duración de un experimento como algo que se dimensiona de antemano, no indefinido), así que hay un argumento razonable de que el modelo de esta lección, tal como está escrito, es incompleto: le falta un umbral de edad también para los experiment que llevan demasiado tiempo sin cerrar. Este es un buen ejemplo de que un modelo pedagógico como flagDebtReport() capta el caso central, pero un sistema de producción real probablemente necesitaría reglas más completas —incluyendo, quizás, alertar cuando un experimento lleva mucho más tiempo del planeado sin resultado, no solo cuando ya cerró.

Ejercicio 3 — Explica la deuda sin usar la palabra "flag". En dos o tres frases, explica a alguien de negocio por qué Mercado dedica tiempo de ingeniería a revisar y quitar flags viejos, en vez de simplemente dejarlos ahí sin causar ningún error visible. Puedes usar la analogía del tablero eléctrico.

Ver solución

Un ejemplo de respuesta: "Es como los cables sueltos detrás de un tablero eléctrico que ya nadie usa: no causan un cortocircuito por sí solos, pero cada persona nueva que necesita trabajar en esa instalación tiene que perder tiempo averiguando cuáles son seguros de tocar y cuáles todavía alimentan algo real. Con el tiempo, mientras más cables sueltos se acumulan, más lento y más riesgoso se vuelve cualquier trabajo nuevo, aunque no tenga nada que ver con esos cables en particular." La idea central: el costo de la deuda de flags no es un error inmediato y visible —es la fricción acumulada que cada flag viejo le agrega a cualquier trabajo futuro, sin importar de qué se trate ese trabajo.

Resumen y siguiente paso

En esta lección construiste flagDebtReport(), un modelo que combina el type de un flag (de la lección 6) con su edad y su estado para decidir si es candidato a deuda: sobre cuatro flags de Mercado, dos resultaron deuda —newCheckoutLayout, un release al 100% desde hace 96 días, y checkoutVariantB, un experimento ya cerrado— y dos no lo son —recommendations, todavía en rollout activo, y disableSellerPayouts, un operational que se espera que viva indefinidamente—. Viste que el criterio correcto siempre combina tipo y estado, nunca solo la edad en aislamiento.

Antes de avanzar deberías poder: explicar qué hace que un flag sea candidato a deuda, según su tipo; distinguir "borrar del registro" de "quitar el código que lo consulta", como dos pasos separados de la limpieza; y explicar por qué medir deuda por cantidad total de flags, sin distinguir tipo, lleva a decisiones equivocadas.

Con esta lección se cierra el arco de tema del módulo: tienes el mecanismo completo del feature flag —qué lo distingue de un if común (L2), su anatomía y dónde vive (L3), cómo exponer un porcentaje de forma estable (L4), cómo apagarlo todo al instante (L5), sus cuatro tipos (L6), y cuándo retirarlo (L7)—. La lección 8, el proyecto de este módulo, te pide poner en práctica las tres piezas técnicas centrales —el flag, la exposición al 10%, y el kill switch— sobre el caso real de recommendations.

Recursos

  • Pete Hodgson (con Martin Fowler), "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. La sección "Managing Technical Debt" describe explícitamente los feature flags como "inventario que viene con un costo de mantenimiento" y recomienda tratarlos como deuda técnica activa, la base directa de esta lección. En inglés.
  • LaunchDarkly, "Reducing technical debt from feature flags" — launchdarkly.com/docs/guides/flags/technical-debt. Documentación práctica sobre el ciclo de vida de un flag y en qué etapas (como "Launched", equivalente al release al 100% de esta lección) conviene retirarlo del código. En inglés.