6 min read
Tu equipo pasó tres semanas construyendo un nuevo motor de búsqueda para el catálogo. El viernes está listo.
¿Lo despliegan y esperan que funcione perfecto para todos los usuarios al mismo tiempo, con el riesgo de que un bug afecte a toda la base de usuarios de un solo golpe?
¿O esperan al lunes con miedo de que se acumulen más cambios en el mismo deploy?
Ninguna de las dos. La respuesta moderna es Feature Flags (Banderas de Funcionalidad, también llamado Feature Toggle): desplegás el código el viernes, apagado, y decidís cuándo y para quién activarlo, de forma completamente independiente del deploy.
La idea central: separar Deploy de Release
Estos dos conceptos suelen tratarse como sinónimos, y no lo son:
- Deploy (despliegue): el código nuevo está corriendo en producción.
- Release (lanzamiento): los usuarios pueden ver y usar la funcionalidad nueva.
Sin Feature Flags, ambos ocurren en el mismo momento — el deploy ES el release.
Con Feature Flags, el código puede estar desplegado en producción durante días sin que nadie lo vea, hasta que decidís activarlo.
Esto habilita estrategias que sin flags son mucho más riesgosas: rollout gradual por porcentaje de usuarios, pruebas A/B, activación instantánea sin esperar el próximo ciclo de deploy, y — la más valiosa en un incidente — apagar la funcionalidad al instante sin necesidad de revertir código ni esperar un nuevo despliegue.
El diagrama

Guía paso a paso
Paso 1 — Envolver el código nuevo detrás de un flag
En lugar de reemplazar directamente el código viejo, agregá una condición que decida cuál camino ejecutar según el estado del flag.
Paso 2 — Definir la estrategia de segmentación
¿El flag se activa globalmente (on/off), por porcentaje de usuarios, por atributo del usuario (país, plan de suscripción), o por lista específica (usuarios beta)?
Paso 3 — Desplegar con el flag apagado
El deploy ocurre sin riesgo: el código nuevo existe en producción pero nadie lo ejecuta todavía.
Paso 4 — Activar gradualmente y monitorear
Subí el porcentaje de forma incremental (5% → 25% → 100%), observando métricas de error en cada escalón antes de continuar.
Paso 5 — Eliminar el flag una vez estabilizado
Este paso se olvida con frecuencia. Un flag que ya está en 100% permanente durante meses es deuda técnica — el código condicional sigue ahí, complicando la lectura, sin necesidad real.
El código
Implementamos un sistema simple de Feature Flags con activación por porcentaje de usuarios, usando un hash determinístico para que el mismo usuario siempre caiga del mismo lado.
import lombok.AllArgsConstructor;
import java.util.concurrent.ConcurrentHashMap;
// --- Configuración de un flag individual ---
class FeatureFlag {
private final String name;
private volatile boolean enabled;
private volatile int rolloutPercentage; // 0-100
public FeatureFlag(String name, boolean enabled, int rolloutPercentage) {
this.name = name;
this.enabled = enabled;
this.rolloutPercentage = rolloutPercentage;
}
public boolean isEnabledFor(String userId) {
if (!enabled) {
return false;
}
if (rolloutPercentage >= 100) {
return true;
}
// Hash determinístico: el mismo usuario siempre cae en el mismo lado
int userBucket = Math.abs((name + userId).hashCode() % 100);
return userBucket < rolloutPercentage;
}
public void setRolloutPercentage(int percentage) {
this.rolloutPercentage = percentage;
System.out.println("[FeatureFlag:" + name + "] Rollout actualizado a " + percentage + "%");
}
public void kill() {
this.enabled = false;
System.out.println("[FeatureFlag:" + name + "] DESACTIVADO — kill switch activado");
}
}
// --- El servicio central de Feature Flags ---
class FeatureFlagService {
private final ConcurrentHashMap<String, FeatureFlag> flags = new ConcurrentHashMap<>();
public void register(String name, boolean enabled, int rolloutPercentage) {
flags.put(name, new FeatureFlag(name, enabled, rolloutPercentage));
}
public boolean isEnabled(String flagName, String userId) {
FeatureFlag flag = flags.get(flagName);
return flag != null && flag.isEnabledFor(userId);
}
public FeatureFlag get(String flagName) {
return flags.get(flagName);
}
}
// --- Servicio de búsqueda: código viejo y nuevo conviven detrás del flag ---
@AllArgsConstructor
class SearchService {
private final FeatureFlagService featureFlags;
public String search(String userId, String query) {
if (featureFlags.isEnabled("nuevo_motor_busqueda", userId)) {
return searchWithNewEngine(query);
}
return searchWithLegacyEngine(query);
}
private String searchWithLegacyEngine(String query) {
return "[Motor viejo] Resultados para: " + query;
}
private String searchWithNewEngine(String query) {
return "[Motor nuevo] Resultados mejorados para: " + query;
}
}
// --- Main ---
public class FeatureFlagsDemo {
public static void main(String[] args) {
FeatureFlagService featureFlags = new FeatureFlagService();
featureFlags.register("nuevo_motor_busqueda", true, 30); // arranca en 30% de rollout
SearchService searchService = new SearchService(featureFlags);
System.out.println("=== Rollout al 30% ===");
String[] userIds = {"user-1", "user-2", "user-3", "user-4", "user-5"};
for (String userId : userIds) {
System.out.println(userId + " → " + searchService.search(userId, "laptop"));
}
System.out.println("\n=== Se sube el rollout al 100% tras validar métricas ===");
featureFlags.get("nuevo_motor_busqueda").setRolloutPercentage(100);
for (String userId : userIds) {
System.out.println(userId + " → " + searchService.search(userId, "laptop"));
}
System.out.println("\n=== Incidente detectado: kill switch ===");
featureFlags.get("nuevo_motor_busqueda").kill();
System.out.println("user-1 → " + searchService.search("user-1", "laptop"));
}
}
Output esperado (los usuarios asignados a cada bucket pueden variar por el hash):
=== Rollout al 30% ===
user-1 → [Motor viejo] Resultados para: laptop
user-2 → [Motor nuevo] Resultados mejorados para: laptop
user-3 → [Motor viejo] Resultados para: laptop
user-4 → [Motor viejo] Resultados para: laptop
user-5 → [Motor viejo] Resultados para: laptop
=== Se sube el rollout al 100% tras validar métricas ===
[FeatureFlag:nuevo_motor_busqueda] Rollout actualizado a 100%
user-1 → [Motor nuevo] Resultados mejorados para: laptop
user-2 → [Motor nuevo] Resultados mejorados para: laptop
user-3 → [Motor nuevo] Resultados mejorados para: laptop
user-4 → [Motor nuevo] Resultados mejorados para: laptop
user-5 → [Motor nuevo] Resultados mejorados para: laptop
=== Incidente detectado: kill switch ===
[FeatureFlag:nuevo_motor_busqueda] DESACTIVADO — kill switch activado
user-1 → [Motor viejo] Resultados para: laptop
Fijate en el último bloque: ante un incidente, apagar el flag revierte la funcionalidad al instante, sin necesidad de un rollback de código ni de esperar un nuevo deploy.
Para tener en cuenta en producción
- Herramientas dedicadas superan la implementación casera: LaunchDarkly, Unleash (open source) y Split.io ofrecen segmentación avanzada, auditoría de cambios y dashboards, evitando reinventar toda esta infraestructura.
- Los flags de larga duración son deuda técnica: un flag “temporal” que sigue en el código dos años después es una señal de alerta. Definí un plan de limpieza desde que creás el flag, no cuando ya se volvió un problema.
- Testeá ambos caminos, no solo el nuevo: mientras el flag existe, tu código tiene efectivamente dos rutas de ejecución. Ambas necesitan cobertura de pruebas hasta que una desaparezca.
- Cuidado con la explosión combinatoria: si tenés diez flags activos simultáneamente, técnicamente tenés 2^10 combinaciones posibles de comportamiento. Mantené el número de flags activos concurrentes bajo control.
Conclusión
Feature Flags cambia la pregunta de “¿está listo el código para desplegarse?” a “¿está lista la funcionalidad para que la vean los usuarios?” — dos preguntas que parecen la misma pero no lo son, y separarlas es lo que permite desplegar con frecuencia sin desplegar con miedo.
Recordá suscribirte aquí para recibir los próximos posts directamente en tu correo.
Referencias:
Continuous Delivery — Jez Humble, David Farley
Martin Fowler — Feature Toggles: https://martinfowler.com/articles/feature-toggles.html