Saltar al contenido

Feature flags a escala

Los feature flags desacoplan el despliegue del lanzamiento: así puedes implementarlos sin crear un enredo de lógica condicional repartida por todo el código.

3 min de lectura
Árbol de decisiones de feature flags que muestra cómo las banderas controlan rutas de código en producción

Los feature flags son una de las herramientas más poderosas en la entrega moderna de software. Te permiten desplegar funcionalidades incompletas en producción, ejecutar pruebas A/B, desplegar cambios de forma gradual y desactivar funcionalidades problemáticas al instante — sin volver a desplegar. Pero sin disciplina, los flags se convierten en deuda técnica que hace más difícil razonar sobre cada ruta de código.

La implementación más simple

En esencia, un feature flag es una condicional. La cuestión es de dónde viene el valor del flag y cómo se gestiona.

tstypescript
// ❌ Hard-coded flag — requires redeployment to change
const NEW_CHECKOUT = true;
 
if (NEW_CHECKOUT) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}
 
// ✅ Runtime flag — changeable without deployment
const flags = await getFeatureFlags(userId);
 
if (flags.isEnabled("new-checkout")) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}

Los flags en tiempo de ejecución son el objetivo. Se pueden alternar en segundos desde un panel, comparado con minutos u horas de un despliegue.

Tipos de flags

No todos los flags sirven para el mismo propósito. Categorizarlos determina su ciclo de vida y estrategia de limpieza.

TipoPropósitoVida útilEjemplo
Flag de lanzamientoControlar funcionalidades incompletasDías a semanasnew-checkout
Flag de experimentoPruebas A/BSemanas a mesescheckout-redesign-v2
Flag de operacionesInterruptor de cargaPermanentedisable-search-indexing
Flag de permisosFuncionalidades por nivel de usuarioPermanentepremium-analytics

Los flags de lanzamiento deben ser de corta duración y limpiarse de forma agresiva. Los flags de experimento viven durante el período de prueba. Los flags de operaciones y permisos son infraestructura permanente.

Construyendo un servicio de flags

tstypescript
interface FeatureFlag {
  key: string;
  enabled: boolean;
  rolloutPercentage: number;
  allowedUserIds: string[];
  rules: FlagRule[];
}
 
interface FlagRule {
  attribute: string;
  operator: "eq" | "in" | "gt" | "lt";
  value: unknown;
}
 
class FeatureFlagService {
  private flags: Map<string, FeatureFlag>;
 
  constructor(flags: FeatureFlag[]) {
    this.flags = new Map(flags.map((f) => [f.key, f]));
  }
 
  isEnabled(key: string, context: FlagContext): boolean {
    const flag = this.flags.get(key);
    if (!flag) return false;
    if (!flag.enabled) return false;
 
    // Explicit allow list
    if (flag.allowedUserIds.includes(context.userId)) return true;
 
    // Rule-based targeting
    if (flag.rules.length > 0) {
      return flag.rules.every((rule) => this.evaluateRule(rule, context));
    }
 
    // Percentage rollout — deterministic per user
    if (flag.rolloutPercentage < 100) {
      const hash = this.hashUserForFlag(context.userId, key);
      return hash < flag.rolloutPercentage;
    }
 
    return true;
  }
 
  private hashUserForFlag(userId: string, flagKey: string): number {
    // Deterministic hash — same user always gets same result
    let hash = 0;
    const input = `${userId}:${flagKey}`;
    for (let i = 0; i < input.length; i++) {
      hash = (hash * 31 + input.charCodeAt(i)) | 0;
    }
    return Math.abs(hash) % 100;
  }
 
  private evaluateRule(rule: FlagRule, context: FlagContext): boolean {
    const value = context[rule.attribute];
    switch (rule.operator) {
      case "eq": return value === rule.value;
      case "in": return (rule.value as unknown[]).includes(value);
      case "gt": return (value as number) > (rule.value as number);
      case "lt": return (value as number) < (rule.value as number);
      default: return false;
    }
  }
}

El despliegue por porcentaje basado en hash es crucial: garantiza que un usuario vea consistentemente la misma variante sin almacenar asignaciones. Aumenta el porcentaje para exponer gradualmente a más usuarios.

Despliegues graduales

La forma más segura de lanzar una funcionalidad es de forma incremental: primero usuarios internos, luego un pequeño porcentaje y después más amplio.

tstypescript
// Rollout strategy for a major feature
const rolloutStages = [
  { percentage: 0, allowList: ["internal-team-ids"], duration: "2 days" },
  { percentage: 5, allowList: [], duration: "3 days" },
  { percentage: 25, allowList: [], duration: "2 days" },
  { percentage: 50, allowList: [], duration: "2 days" },
  { percentage: 100, allowList: [], duration: "permanent" },
];
tstypescript
// Monitor error rates at each stage
function shouldProceedToNextStage(metrics: StageMetrics): boolean {
  const errorRateThreshold = 0.01; // 1%
  const latencyP99Threshold = 500; // ms
 
  return (
    metrics.errorRate < errorRateThreshold &&
    metrics.latencyP99 < latencyP99Threshold &&
    metrics.userComplaints === 0
  );
}

Si las tasas de error suben al 5%, retrocedes al 0% al instante — sin despliegue, sin tiempo de inactividad.

El problema de la limpieza

El mayor riesgo con los feature flags no es la funcionalidad, sino los flags mismos. Los flags obsoletos se acumulan y crean una explosión combinatoria imposible de probar.

tstypescript
// ❌ Stale flags — nobody knows if these are still needed
if (flags.isEnabled("new-checkout")) {
  if (flags.isEnabled("checkout-v2-variant-b")) {
    if (flags.isEnabled("express-checkout")) {
      // How many code paths is this? 2^3 = 8 combinations to test
    }
  }
}
 
// ✅ Clean up flags after full rollout
// 1. Roll out to 100%
// 2. Monitor for 1 week
// 3. Remove the flag check, keep only the new code path
// 4. Delete the flag from the configuration

Añade fechas de expiración a los flags de lanzamiento. Rastrea la antigüedad del flag en tu panel. Un flag de lanzamiento mayor de 30 días es deuda técnica.

Pruebas con flags

Los feature flags complican las pruebas. Necesitas probar ambas rutas — y, idealmente, la transición entre ellas.

tstypescript
describe("checkout", () => {
  it("renders new checkout when flag is enabled", () => {
    const flags = new FeatureFlagService([
      { key: "new-checkout", enabled: true, rolloutPercentage: 100, allowedUserIds: [], rules: [] },
    ]);
 
    const result = renderCheckout(flags, testContext);
    expect(result).toContain("Express Checkout");
  });
 
  it("renders legacy checkout when flag is disabled", () => {
    const flags = new FeatureFlagService([
      { key: "new-checkout", enabled: false, rolloutPercentage: 0, allowedUserIds: [], rules: [] },
    ]);
 
    const result = renderCheckout(flags, testContext);
    expect(result).toContain("Standard Checkout");
  });
});

Conclusiones clave

  1. Los feature flags desacoplan el despliegue del lanzamiento — despliega diariamente, lanza cuando esté listo
  2. Categoriza los flags por tipo — los flags de lanzamiento son temporales, los de operaciones son permanentes
  3. Los despliegues graduales reducen el riesgo — empieza en 0%, aumenta según las métricas
  4. El despliegue por porcentaje basado en hash garantiza que los usuarios vean consistentemente la misma variante
  5. Limpia los flags de forma agresiva — los flags obsoletos crean explosiones combinatorias imposibles de probar
  6. Cada flag necesita un propietario y una fecha de expiración para flags de lanzamiento
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX