Feature Flags (Banderas de Funcionalidad)
Cómo implementar feature toggles para desplegar, probar y revertir funcionalidad de forma segura sin desplegar código.
Visión General
Los feature flags (o feature toggles) desacoplan el despliegue del lanzamiento. Permiten mergear funciones incompletas a main, habilitarlas para un subconjunto de usuarios, medir impacto y revertir instantáneamente sin un nuevo despliegue. Esta receta cubre la construcción de un servicio ligero de flags, estrategias de rollout (booleano, porcentaje, targeting de usuarios) y patrones de limpieza segura en Python, JavaScript y Java.
Cuándo Usar
Usa este recurso cuando:
- Despliegues gradualmente una función de alto riesgo para monitorear errores
- Ejecutes tests A/B para comparar dos implementaciones de una función
- Despliegues código incompleto a
mainsin exponerlo a usuarios - Necesites un kill-switch instantáneo para una función causando problemas en producción
Solución
Python
import hashlib
import random
from typing import Callable
class FeatureFlags:
def __init__(self, config: dict[str, any]):
self.config = config
def is_enabled(self, flag: str, user_id: str = None) -> bool:
rule = self.config.get(flag, False)
if isinstance(rule, bool):
return rule
if isinstance(rule, dict):
# Rollout por porcentaje
if "percentage" in rule and user_id:
bucket = self._hash_bucket(user_id, flag)
return bucket < rule["percentage"]
# Usuarios target
if "users" in rule and user_id:
return user_id in rule["users"]
# Grupos target
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 lookup de membresía de grupo
return False
# Uso
flags = FeatureFlags({
"new_dashboard": True,
"beta_search": {"percentage": 10}, # 10% rollout
"vip_feature": {"users": ["user_123"]}, # dirigido
"admin_tools": {"groups": ["admins"]}, # por grupos
})
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; // hook para membresía de grupo
}
}
// Uso
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; // hook para lookup de membresía
}
// Uso
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%
}
}
Explicación
- Flags booleanos son interruptores on/off para toda la aplicación. Úsalos para kill-switches y dark launches.
- Rollouts por porcentaje asignan usuarios a buckets vía un hash determinístico de
(flag_name + user_id) % 100. El mismo usuario siempre ve el mismo bucket, asegurando experiencias consistentes. - Targeting de usuarios whitelistea explícitamente usuarios (beta testers, equipo interno) para acceso temprano.
- Targeting de grupos verifica membresía en roles o segmentos (admin, premium, región geográfica).
- Hashing determinístico es crítico: la asignación aleatoria haría que un mismo usuario alterne entre variantes en cada request, rompiendo UX y analytics.
Variantes
| Estrategia | Tipo de Regla | Ideal Para |
|---|---|---|
| Booleano | true / false | Kill-switches, rollbacks de emergencia |
| Porcentaje | {"percentage": 10} | Rollout gradual, releases canary |
| Target por Usuario | {"users": ["id1"]} | Programas beta, dogfooding interno |
| Target por Grupo | {"groups": ["premium"]} | Tiers de función, acceso basado en roles |
| A/B Test | {"percentage": 50, "variant": "B"} | Comparar dos implementaciones |
Mejores Prácticas
- Mantén los flags de corta duración — los flags permanentes se convierten en deuda técnica. Elimínalos y las rutas de código muerto una vez que una función esté completamente desplegada.
- Usa bucketing determinístico — hashea
(flag + user_id)para que el mismo usuario siempre obtenga la misma experiencia, evitando alternancias. - Loguea evaluaciones de flags — registra qué usuarios vieron qué variante para debugging y correlación de analytics.
- Default a off — si el servicio de flags es inalcanzable, la función debería estar deshabilitada para prevenir exposición inesperada.
- Audita cambios de flags — trata los cambios de configuración de flags como despliegues de producción; requiere code review y trackea en control de versiones.
Errores Comunes
- Dejar flags en la base de código permanentemente, creando un laberinto de rutas de código muerto.
- Usar bucketing aleatorio en vez de hashing determinístico, causando experiencias de usuario inconsistentes.
- No manejar el caso donde el servicio de config de flags está caído, provocando fallos en cascada.
- Hacer over-targeting de flags a usuarios individuales en vez de grupos, haciendo la gestión no escalable.
- Lanzar una función bajo un flag sin monitoreo ni alerting, perdiendo problemas de producción.
Preguntas Frecuentes
¿Cuándo debería eliminar un feature flag?
Elimina el flag y sus ramas condicionales una vez que la función es estable para el 100% de usuarios y ha estado corriendo en producción sin problemas por 1-2 ciclos de release. Los flags que viven más de un mes tras el despliegue completo se convierten en deuda técnica.
¿En qué se diferencian los feature flags de los settings de configuración?
Los settings de configuración son típicamente estáticos y aplican globalmente (valores de timeout, límites de funciones). Los feature flags son dinámicos, scoped por usuario, y diseñados para toggle rápido sin redeployment. Los flags evalúan por request; la config se carga al inicio.
¿Puedo usar feature flags para autorización?
No. Los feature flags controlan visibilidad y rollout de funciones; la autorización controla derechos de acceso. No uses flags para reforzar límites de seguridad. Un usuario que evade un chequeo de flag no debería ganar acceso no autorizado a datos u operaciones sensibles.
Recursos Relacionados
Background Jobs
How to schedule and run background jobs using cron, task queues, and workers.
RecipeCLI Tool with Argument Parsing
How to build a professional command-line interface with argument parsing, flags, and subcommands.
RecipeEnvironment Variables
How to read, set, and manage environment variables securely across Python, JavaScript, and Java.
RecipeHealth Check Endpoint
How to implement a production-ready health check endpoint for monitoring and load balancers.
RecipeParse and Validate YAML/JSON Configuration
How to parse and validate application configuration files using YAML and JSON schemas.