Feature Flags: despliega sin miedo y controla quién ve qué
Imagina que llevas dos semanas desarrollando una funcionalidad nueva. Todo va bien en staging. Haces el merge, despliegas a producción y… algo falla. No en la lógica que tocaste tú. Falla en un flujo que nadie había tocado. A las 11 de la noche. Con 40.000 usuarios activos.
El rollback tarda 8 minutos. Esos 8 minutos se hacen eternos.
Lo que acabo de describir es el escenario que los feature flags están diseñados para evitar. No previenen los bugs, pero sí te dan un interruptor de emergencia para apagarlos sin necesidad de un despliegue.
Qué es un feature flag
Un feature flag (también llamado feature toggle o feature switch) es una condición en el código que decide si una funcionalidad está activa o no. Nada más.
// La idea más simple posible
if (featureFlags.isEnabled('new_checkout')) {
return <CheckoutV2 />;
}
return <CheckoutV1 />;
Parece trivial. Pero esta pequeña abstracción cambia completamente cómo puedes trabajar en equipo, cómo haces releases y cómo reduces el riesgo de cada despliegue.
La diferencia entre un flag y un if normal es que el valor del flag se puede cambiar sin tocar el código. Desde una base de datos, desde un panel de administración, desde una llamada a una API. El código ya está desplegado; lo que cambia es la configuración.
Los cuatro tipos que deberías conocer
No todos los flags son iguales. Martin Fowler los clasifica en cuatro categorías según su vida útil y propósito.
Release flags (los más comunes)
El caso de uso más habitual. Despliegas el código de la nueva funcionalidad, pero la mantienes apagada. Cuando quieres activarla, cambias el flag. Sin código nuevo, sin despliegue, sin nervios.
// ✅ Despliegas primero, activas después
async function renderDashboard(userId: string) {
const flags = await getFlags(userId);
if (flags.isEnabled('new_dashboard')) {
return <DashboardV2 />;
}
return <DashboardV1 />;
}
La vida útil de este tipo de flag es corta: cuando la funcionalidad está 100% activa para todos los usuarios, el flag se elimina del código.
Experiment flags (A/B testing)
Sirven para comparar dos versiones de algo con usuarios reales. El sistema asigna de forma aleatoria (o basándose en el id del usuario) qué variante recibe cada persona.
function getCheckoutVariant(userId: string): 'A' | 'B' {
// Asignación determinista: mismo usuario → siempre misma variante
const hash = simpleHash(userId) % 100;
return hash < 50 ? 'A' : 'B';
}
// En el componente
const variant = getCheckoutVariant(currentUser.id);
trackExperiment('checkout_test', variant);
return variant === 'B' ? <CheckoutOptimizado /> : <CheckoutOriginal />;
Ops flags (kill switches)
Interruptores de emergencia para desactivar partes del sistema bajo carga o durante un incidente. Son permanentes: no desaparecen cuando termina el release.
// Un ops flag típico para proteger una funcionalidad costosa
async function searchWithAI(query: string) {
if (!opsFlags.isEnabled('ai_search')) {
// Fallback al buscador clásico si el flag está apagado
return classicSearch(query);
}
return await aiPoweredSearch(query);
}
Permission flags (flags de acceso)
Controlan qué funcionalidades están disponibles según el plan o rol del usuario. El flag no es temporal: es parte de la lógica de negocio.
function canAccessAnalytics(user: User): boolean {
return permissionFlags.isEnabled('advanced_analytics', {
targeting: { plan: user.plan },
});
}
Implementación: de lo más simple a lo razonable
Nivel 0: variables de entorno
El punto de entrada. Sin dependencias, funciona en cualquier proyecto.
// ❌ Directamente en el código (no recomendado fuera de flags de entorno)
const NEW_FEATURE = process.env.ENABLE_NEW_FEATURE === 'true';
El problema: para cambiar el valor necesitas reiniciar el servidor o hacer un redespliegue. No tienes forma de activarlo para un usuario específico. Sirve para flags de entorno (dev vs prod), pero no para release flags en producción.
Nivel 1: flags en base de datos
Guardas los flags en una tabla y los lees al arrancar (o con caché TTL). Ya puedes cambiarlos sin redesplegar.
// Schema mínimo
// tabla: feature_flags
// columnas: key TEXT, enabled BOOLEAN, targeting JSONB
interface FeatureFlag {
key: string;
enabled: boolean;
targeting?: {
segments?: string[]; // 'beta', 'premium', etc.
percentage?: number; // 0-100 para rollouts graduales
userIds?: string[]; // lista blanca explícita
};
}
class FeatureFlagService {
private cache: Map<string, FeatureFlag> = new Map();
private lastFetch = 0;
private readonly TTL = 30_000; // 30 segundos
async isEnabled(key: string, context?: { userId?: string; segment?: string }): Promise<boolean> {
const flag = await this.getFlag(key);
if (!flag || !flag.enabled) return false;
if (!flag.targeting) return true;
return this.evaluateTargeting(flag.targeting, context);
}
private evaluateTargeting(
targeting: NonNullable<FeatureFlag['targeting']>,
context?: { userId?: string; segment?: string },
): boolean {
if (targeting.userIds && context?.userId) {
if (targeting.userIds.includes(context.userId)) return true;
}
if (targeting.segments && context?.segment) {
if (targeting.segments.includes(context.segment)) return true;
}
if (targeting.percentage !== undefined && context?.userId) {
const bucket = this.getUserBucket(context.userId);
return bucket < targeting.percentage;
}
return false;
}
private getUserBucket(userId: string): number {
// Hash determinista: mismo usuario → siempre mismo bucket
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = (hash * 31 + userId.charCodeAt(i)) >>> 0;
}
return hash % 100;
}
private async getFlag(key: string): Promise<FeatureFlag | undefined> {
const now = Date.now();
if (now - this.lastFetch > this.TTL) {
await this.refreshCache();
}
return this.cache.get(key);
}
private async refreshCache() {
const flags = await db.query<FeatureFlag>('SELECT * FROM feature_flags WHERE enabled = true');
this.cache.clear();
for (const flag of flags) this.cache.set(flag.key, flag);
this.lastFetch = Date.now();
}
}
Nivel 2: rollout gradual
Una de las técnicas más potentes. En lugar de activar para todos de golpe, vas subiendo el porcentaje poco a poco. Si algo explota, bajas a 0% sin hacer nada más.
// Activar para el 5% de usuarios primero
const flag: FeatureFlag = {
key: 'new_payment_flow',
enabled: true,
targeting: { percentage: 5 },
};
// Una semana sin incidentes → subir al 25%
// Dos semanas → 50% → 100% → eliminar el flag
El truco del getUserBucket con hash es importante: garantiza que si un usuario está en el 5%, también estará en el 25% y en el 50%. Sin eso, los usuarios saltarían de variante entre peticiones y la experiencia sería inconsistente.
El simulador
Prueba a activar y desactivar los flags de abajo. Fíjate especialmente en cómo el banner beta y el checkout v2 solo aparecen cuando el usuario es del segmento “Beta” o “Premium”, aunque el flag esté activado. Eso es el targeting por segmento en acción.
Simulador interactivo de Feature Flags
Activa o desactiva flags y cambia el segmento de usuario para ver en tiempo real cómo se comporta la aplicación. Fíjate en cómo el mismo flag puede afectar (o no) a usuarios distintos según las reglas de targeting.
¹ Solo visible para usuarios Beta o Premium (targeting por segmento).
El ciclo de vida de un flag
Un error muy común es tratar los feature flags como algo permanente. No lo son (salvo los ops flags y permission flags). Un flag tiene fecha de caducidad.
Crear → Desplegar (OFF) → Activar gradualmente → 100% ON → Eliminar
Si no eliminas los flags muertos, el código se llena de condiciones que nadie sabe si siguen siendo necesarias. En sistemas grandes esto se convierte en deuda técnica seria.
Un flag que lleva más de 3 meses activo al 100% sin haberse eliminado es un bug esperando a ocurrir. Alguien lo tocará sin entender qué hace.
La solución práctica: añade una fecha de expiración a cada flag y un test que falle si el flag sigue en el código pasada esa fecha.
// test/feature-flags.test.ts
import { flags } from '../src/config/flags';
test('no hay flags expirados', () => {
const today = new Date();
const expired = flags.filter(
(f) => f.expiresAt && new Date(f.expiresAt) < today,
);
expect(expired).toEqual([]);
});
Herramientas reales
Puedes implementar tu propia solución como hemos visto, pero si el equipo crece hay herramientas específicas que merece la pena conocer.
| Herramienta | Tipo | Punto fuerte |
|---|---|---|
| LaunchDarkly | SaaS | La más completa, streaming en tiempo real, targeting avanzado |
| Unleash | Open source | Self-hosted, muy flexible, con SDKs para casi todo |
| Flipt | Open source | Moderno, gRPC, fácil de desplegar con Docker |
| PostHog | SaaS/Open source | Combina flags con analítica y A/B testing |
| OpenFeature | Estándar abierto | SDK agnóstico de proveedor, útil si quieres evitar lock-in |
OpenFeature merece una mención especial: es un estándar de la CNCF que define una API común para feature flags. Así puedes cambiar de proveedor sin tocar el código de tu aplicación.
import { OpenFeature } from '@openfeature/server-sdk';
// La aplicación siempre usa la misma API
const client = OpenFeature.getClient();
const showNewDashboard = await client.getBooleanValue('new_dashboard', false, {
targetingKey: user.id,
});
Lo que no debes hacer con los flags
Aunque son muy útiles, los feature flags tienen sus anti-patrones.
No uses flags para lógica de negocio permanente. Si la condición es “los usuarios premium tienen acceso a X”, eso no es un flag temporal: es una regla de negocio. Modélala como tal en tu dominio, no como un toggle de configuración.
No anides flags. Si tu código tiene if (flagA && flagB && !flagC), algo está mal. Cada combinación multiplica la complejidad y los casos a testear.
// ❌ Esto es una trampa
if (flags.isEnabled('new_ui')) {
if (flags.isEnabled('new_sidebar')) {
if (!flags.isEnabled('legacy_mode')) {
// ¿Alguien sabe qué combinaciones son válidas aquí?
}
}
}
No dejes flags sin documentar. Cada flag debe tener una descripción clara, quién lo creó, para qué sirve y cuándo se puede eliminar. Una línea en el código o en la base de datos es suficiente.
En resumen
Los feature flags son una de esas herramientas que, una vez que las incorporas a tu flujo de trabajo, no puedes imaginar cómo trabajabas sin ellas. El concepto es simple; la implementación básica cabe en 50 líneas de TypeScript; y el beneficio es enorme: separas el despliegue del lanzamiento.
Despliegas cuando el código está listo. Lanzas cuando el negocio está listo.
Si empiezas desde cero, mi recomendación es:
- Implementa la versión de base de datos que vimos arriba. Es suficiente para el 80% de los casos.
- Añade un panel mínimo (o usa la consola de tu ORM) para activar y desactivar flags sin tocar código.
- Pon fecha de expiración a cada flag desde el primer día y añade el test que falle cuando caducan.
- Cuando el equipo crezca y necesites targeting avanzado o streaming en tiempo real, evalúa Unleash o LaunchDarkly.
Lo que no hagas: implementes una solución compleja desde el principio. Empieza simple, evoluciona cuando el dolor sea real.
Preguntas frecuentes
¿Los feature flags afectan al rendimiento? Con caché (TTL de 30-60 segundos en memoria) el impacto es negligible. Sin caché, cada request haría una consulta a la base de datos, lo cual sí sería un problema. Siempre cachea.
¿Cómo testo código con feature flags? Inyecta el servicio de flags como dependencia y en los tests usa un mock con los valores que necesitas. Testea cada rama de forma independiente.
// En tests
const mockFlags = { isEnabled: (key: string) => key === 'new_checkout' };
render(<App flagService={mockFlags} />);
¿Qué hago si necesito activar un flag solo para un usuario concreto?
Añade soporte para userIds en el targeting (lo vimos en el ejemplo). Es útil para dar acceso a un cliente específico antes de un release general o para depurar un problema en producción.
¿Los flags de front-end y back-end deben estar sincronizados? Sí, siempre. Si activas una UI nueva sin activar el endpoint que necesita, o al revés, tendrás inconsistencias. Lo más seguro es que el cliente lea los flags del servidor en cada sesión, no que los evalúe de forma independiente.
¿Cuántos flags es demasiado? No hay un número exacto, pero si tienes más de 20-30 flags activos simultáneamente en un servicio, probablemente estás acumulando deuda. Revisa cuáles llevan más de 3 meses al 100% y elimínalos.
Artículos relacionados
12 jun 2026
Agentes Loops: ReAct, Ralph y el arte de no perder el contexto
Qué son el bucle ReAct y el bucle Ralph, cómo funcionan los agentes de IA por dentro y cuándo elegir cada patrón para construir agentes fiables.
2 jun 2026
Circuit Breaker: el fusible que evita que tu microservicio se hunda
Patrón Circuit Breaker explicado con una simulación interactiva y un ejemplo real con Spring Boot y Resilience4j. Cuándo usarlo y cuándo no.
30 may 2026
Mocks no son Stubs: la diferencia que cambia cómo testeas
Guía práctica sobre test doubles en Spring Boot: cuándo usar mocks, stubs, fakes o spies, y por qué confundirlos genera tests frágiles e inútiles.
Newsletter
Aprende algo útil cada semana.
Ideas sobre programación, arquitectura e IA para mejorar tu código en menos de 5 minutos de lectura.
Sin spam · Baja cuando quieras