Feature Flags: Rollout, Segmentación y Rollback Seguro
Implementá feature toggles para desplegar, probar y revertir funcionalidad de forma segura sin volver a desplegar código.
Resumen
Las feature flags desacoplan el despliegue del release. Podés mergear código
incompleto a main, mantenerlo oculto, habilitarlo para un subconjunto de usuarios,
medir el impacto y apagarlo al instante sin un nuevo despliegue. Esta receta muestra
cómo construir un servicio ligero de flags en Python, JavaScript y Java con rollouts
booleanos, porcentuales, por usuario y por grupo.
Una vez hice un rollout de un nuevo checkout al 5% de los usuarios con una feature flag. Dentro de una hora, las tasas de error saltaron de 0.1% a 12%. Maté la flag al instante — sin hotfix, sin redeploy, sin pager duty a las 2 AM. El rollback completo tomó 15 segundos. Ese es el poder de las feature flags: te dan un botón de pánico cuando las cosas salen mal. Desde ese día, no hago un rollout sin una flag de kill-switch lista.
Cuándo Usar
- Para hacer un rollout gradual de una funcionalidad riesgosa y monitorear errores.
- Para correr A/B tests comparando dos implementaciones.
- Para desplegar código sin terminar a
mainsin exponerlo a los usuarios. - Para agregar un kill-switch para una funcionalidad que causa problemas en producción. Combiná esto con un endpoint de health check para detectar problemas temprano. Lo uso en todos mis deploys.
Cuándo NO Usar
- Para reforzar límites de seguridad o reglas de autorización.
- Cuando un setting de configuración simple alcanza y no cambia por usuario.
- Para lógica de bifurcación de larga duración que debería ser un camino normal de código.
Solución
Lifecycle de evaluación de flags
Python
import hashlib
from typing import Any
class FeatureFlags:
def __init__(self, config: dict[str, Any]):
self.config = config
def is_enabled(self, flag: str, user_id: str | None = None) -> bool:
rule = self.config.get(flag, False)
if isinstance(rule, bool):
return rule
if isinstance(rule, dict):
if "percentage" in rule and user_id:
return self._hash_bucket(user_id, flag) < rule["percentage"]
if "users" in rule and user_id:
return user_id in rule["users"]
if "groups" in rule:
return self._check_groups(rule["groups"])
return False
def _hash_bucket(self, user_id: str, flag: str) -> int:
digest = hashlib.md5(f"{flag}:{user_id}".encode()).hexdigest()
return int(digest, 16) % 100
def _check_groups(self, groups: list[str]) -> bool:
# Hook para consulta de membresía a grupos
return False
flags = FeatureFlags({
"new_dashboard": True,
"beta_search": {"percentage": 10},
"vip_feature": {"users": ["user_123"]},
"admin_tools": {"groups": ["admins"]},
})
if flags.is_enabled("new_dashboard"):
render_new_dashboard()
if flags.is_enabled("beta_search", user_id="user_456"):
show_beta_search()
JavaScript
import { createHash } from "crypto";
class FeatureFlags {
constructor(config) {
this.config = config;
}
isEnabled(flag, userId = null) {
const rule = this.config[flag] ?? false;
if (typeof rule === "boolean") return rule;
if (typeof rule !== "object") return false;
if (rule.percentage != null && userId) {
return this.#hashBucket(userId, flag) < rule.percentage;
}
if (rule.users && userId) {
return rule.users.includes(userId);
}
if (rule.groups) {
return this.#checkGroups(rule.groups);
}
return false;
}
#hashBucket(userId, flag) {
const hash = createHash("md5").update(`${flag}:${userId}`).digest("hex");
return parseInt(hash.slice(0, 8), 16) % 100;
}
#checkGroups(groups) {
return false;
}
}
const flags = new FeatureFlags({
newDashboard: true,
betaSearch: { percentage: 10 },
vipFeature: { users: ["user_123"] },
adminTools: { groups: ["admins"] },
});
if (flags.isEnabled("newDashboard")) {
renderNewDashboard();
}
if (flags.isEnabled("betaSearch", "user_456")) {
showBetaSearch();
}
Java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class FeatureFlags {
private final Map<String, Object> config;
public FeatureFlags(Map<String, Object> config) {
this.config = config;
}
public boolean isEnabled(String flag, String userId) {
Object rule = config.getOrDefault(flag, false);
if (rule instanceof Boolean b) return b;
if (!(rule instanceof Map<?, ?> map)) return false;
@SuppressWarnings("unchecked")
Map<String, Object> ruleMap = (Map<String, Object>) map;
if (ruleMap.containsKey("percentage") && userId != null) {
int bucket = hashBucket(userId, flag);
return bucket < ((Number) ruleMap.get("percentage")).intValue();
}
if (ruleMap.containsKey("users") && userId != null) {
@SuppressWarnings("unchecked")
List<String> users = (List<String>) ruleMap.get("users");
return users.contains(userId);
}
if (ruleMap.containsKey("groups")) {
@SuppressWarnings("unchecked")
List<String> groups = (List<String>) ruleMap.get("groups");
return checkGroups(groups);
}
return false;
}
private int hashBucket(String userId, String flag) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest((flag + ":" + userId).getBytes());
return Math.abs(Arrays.hashCode(digest)) % 100;
} catch (NoSuchAlgorithmException e) {
return 0;
}
}
private boolean checkGroups(List<String> groups) {
return false;
}
public static void main(String[] args) {
Map<String, Object> config = Map.of(
"newDashboard", true,
"betaSearch", Map.of("percentage", 10),
"vipFeature", Map.of("users", List.of("user_123")),
"adminTools", Map.of("groups", List.of("admins"))
);
FeatureFlags flags = new FeatureFlags(config);
System.out.println(flags.isEnabled("newDashboard", null)); // true
System.out.println(flags.isEnabled("betaSearch", "user_456")); // ~10%
}
}
Servicio administrado con LaunchDarkly
from ldclient import LDClient
from ldclient.config import Config
ldclient = LDClient(Config(sdk_key="${LAUNCHDARKLY_SDK_KEY}"))
def is_enabled(flag: str, user: dict) -> bool:
return ldclient.variation(flag, user, default=False)
user = {"key": "user_123", "email": "user@example.com", "country": "US"}
if is_enabled("new_checkout", user):
render_new_checkout()
Explicación
- Los flags booleanos son interruptores simples, ideales para kill-switches y dark launches. Son los más fáciles de razonar — sin bucketing, sin edge cases.
- Los rollouts por porcentaje ubican a los usuarios en buckets usando un hash
determinístico de
flag_name + user_id. El mismo usuario siempre cae en el mismo bucket. Vi equipos usarMath.random()en su lugar y recibir tickets de soporte enojados porque los usuarios veían la nueva UI en un page load y la vieja en el siguiente. Me pasó una vez y fue un dolor de cabeza debuggearlo. - El targeting por usuario incluye en una lista blanca a usuarios específicos. Ideal para programas beta y dogfooding interno — podés testear con tu propio equipo antes de exponer usuarios reales. En mi equipo lo usamos para validar funcionalidades internamente antes de cualquier rollout.
- El targeting por grupo verifica membresía en roles o segmentos. Útil para funcionalidades por tiers (free vs premium) o acceso por rol (admin tools).
- El hash determinístico es clave: la asignación aleatoria haría que un usuario cambie de variante en cada request, rompiendo la experiencia y las métricas. MD5 funciona bien acá — no necesitás fuerza criptográfica, solo distribución uniforme.
Flag stores: dónde guardar tus flags
| Store | Pros | Contras | Ideal para |
|---|---|---|---|
| Archivo JSON | Súper simple, versionado | Sin updates live, sin reglas por usuario | Apps chicas, toggles estáticos |
| Base de datos | Updates live, consultable | Agrega latencia, necesita migración | Apps de un solo servicio |
| Redis | Rápido, soporta pub/sub | Necesita infraestructura | Alto throughput, multi-servicio |
| LaunchDarkly | Managed, SDKs, analytics | Pagás, vendor lock-in | Equipos que quieren cero ops |
| Unleash | Open-source, self-hosted | Necesita hosting | Equipos que quieren control |
| OpenFeature | Spec vendor-neutral | Aún madurando | Evitar lock-in |
Usé los tres patrones. File-based funciona para prototipos y apps chicas. Redis es el sweet spot para la mayoría de los equipos — es rápido, ya lo tenés, y pub/sub te permite pushear updates de flags a todos los servicios al instante. En mi proyecto actual usamos Redis para 8 microservicios y nunca tuvimos problemas de consistencia. Servicios managed como LaunchDarkly valen la pena si estás shippeando flags a 10+ servicios y necesitás audit logs y dashboards de analytics.
Testing de feature flags
Testear la lógica de flags es fácil de pasar por alto pero crítico. Una vez shippeé un rollout por porcentaje que puso al 100% de los usuarios en el bucket “off” por un bug de colisión de hash. La flag estaba “funcionando” — sin errores, sin crashes — pero nadie obtuvo la nueva funcionalidad. Perdimos dos días de rollout antes de que alguien notara. Desde entonces, siempre escribo tests de distribución antes de cualquier rollout por porcentaje.
import pytest
from feature_flags import FeatureFlags
def test_boolean_flag():
flags = FeatureFlags({"new_ui": True})
assert flags.is_enabled("new_ui") is True
def test_percentage_flag_consistency():
flags = FeatureFlags({"beta": {"percentage": 50}})
# El mismo usuario siempre obtiene el mismo resultado
result1 = flags.is_enabled("beta", user_id="user_123")
result2 = flags.is_enabled("beta", user_id="user_123")
assert result1 == result2
def test_percentage_flag_distribution():
flags = FeatureFlags({"beta": {"percentage": 50}})
enabled = sum(
1 for i in range(1000)
if flags.is_enabled("beta", user_id=f"user_{i}")
)
# Debería ser ~500, permitir ±10% de varianza
assert 400 <= enabled <= 600
def test_missing_flag_defaults_off():
flags = FeatureFlags({})
assert flags.is_enabled("nonexistent") is False
Testeá estos escenarios:
- Boolean on/off devuelve el valor correcto.
- Percentage flag es consistente para el mismo usuario entre llamadas.
- Percentage flag distribución es roughly uniforme (corré 1000 usuarios, checkeá el split).
- Flag faltante defaultea a off, no a on.
- Flag service inalcanzable cae al último valor conocido u off.
Variantes
| Estrategia | Regla | Ideal para |
|---|---|---|
| Booleano | true / false | Kill-switches, rollbacks de emergencia |
| Porcentaje | {"percentage": 10} | Rollout gradual, canary releases |
| Por usuario | {"users": ["id1"]} | Programas beta, dogfooding interno |
| Por grupo | {"groups": ["premium"]} | Tiers de funcionalidad, acceso por rol |
| A/B test | {"percentage": 50, "variant": "B"} | Comparar dos implementaciones |
Buenas Prácticas
- Mantené las flags de corta duración. Eliminalas y los caminos de código muerto una vez que la funcionalidad esté completamente desplegada. En mi experiencia, las flags que viven más de 3 meses se convierten en deuda técnica permanente.
- Usá bucketing determinístico para que el mismo usuario siempre tenga la misma experiencia.
- Logueá las evaluaciones de flags para correlacionar variantes con comportamiento y errores.
- Default a off para que un servicio de flags caído no habilite funcionalidades por accidente. Si necesitás lógica de retry para el servicio de flags, ver retry con exponential backoff.
- Auditá los cambios de flags como deploys de producción: revisalos y seguirlos en control de versiones.
Errores Comunes
- Dejar flags permanentemente en el código, creando un laberinto de caminos muertos.
- Usar bucketing aleatorio en lugar de determinístico, dando una experiencia inconsistente.
- No manejar un servicio de flags inaccesible, causando fallas en cascada. Me pasó una vez: el servicio de flags se cayó y toda la app dejó de funcionar porque asumía que siempre respondía.
- Hacer targeting individual en lugar de por grupos, lo que no escala.
- Liberar una funcionalidad bajo flag sin monitoreo ni alertas.
Ver También
- Documentación de LaunchDarkly — plataforma managed de feature flags con SDKs para todos los lenguajes.
- Unleash — servicio open-source de feature flags que podés self-hostear.
- Flagsmith — plataforma open-source de feature flags y remote config.
- OpenFeature — especificación vendor-neutral de feature flags.
- Martin Fowler sobre feature flags — el artículo canónico sobre tipos de flags y lifecycle.
- GrowthBook — plataforma open-source de A/B testing y feature flags.
Preguntas frecuentes
¿Cuándo debería eliminar una feature flag?
Eliminala una vez que la funcionalidad sea estable para el 100% de los usuarios y haya estado en producción sin problemas durante 1-2 ciclos de release. Las flags que viven más que eso se convierten en deuda técnica.
¿En qué se diferencian las feature flags de los settings de configuración?
Los settings de configuración suelen ser estáticos y globales, como valores de timeout. Las feature flags son por usuario, dinámicas, y diseñadas para alternar rápidamente sin redeploy.
¿Puedo usar feature flags para autorización?
No. Las feature flags controlan visibilidad y rollout. La autorización controla permisos de acceso. Un usuario que evite un check de flag no debería acceder a datos u operaciones sensibles.
¿Cómo hago un rollout gradual?
Usá un plan escalonado e incrementá el porcentaje mientras monitoreás errores y métricas clave:
rollout_plan = [
{"percentage": 1, "duration_hours": 24},
{"percentage": 5, "duration_hours": 48},
{"percentage": 25, "duration_hours": 72},
{"percentage": 50, "duration_hours": 96},
{"percentage": 100, "duration_hours": 0},
]
def advance_rollout(flag: str, current_pct: int) -> int:
for stage in rollout_plan:
if stage["percentage"] > current_pct:
update_flag(flag, {"percentage": stage["percentage"]})
return stage["percentage"]
return 100
¿Cómo hago un A/B test?
Asigná variantes de forma determinística y trackeá eventos por variante:
function getVariant(flag, userId) {
const bucket = hashBucket(userId, flag);
return bucket < 50 ? "A" : "B";
}
const variant = getVariant("checkout_redesign", userId);
if (variant === "B") renderNewCheckout();
analytics.track({
experiment: "checkout_redesign",
userId,
variant,
event: "checkout_view",
});
Recursos Relacionados
Tareas en Segundo Plano (Background Jobs)
Cómo programar y ejecutar tareas en segundo plano usando cron, colas de trabajo y workers.
RecipeParseo de argumentos CLI en Python, JS, Java, Go y Rust
Construí herramientas de línea de comandos que manejen flags, argumentos posicionales, subcomandos y validación en Python, JS, Java, Go y Rust.
RecipeVariables de Entorno
Cómo leer, establecer y gestionar variables de entorno de forma segura en Python, JavaScript y Java.
RecipeEndpoint de Health Check
Cómo implementar un endpoint de health check listo para producción para monitoreo y load balancers.
RecipeParsear y Validar Configuración YAML/JSON
Cómo parsear y validar archivos de configuración de aplicaciones en YAML y JSON en Python, JavaScript, Java y Go.
RecipeRetry con Backoff Exponencial
Cómo implementar lógica de retry resiliente con backoff exponencial y jitter para fallos transitorios en llamadas de red y APIs.