extreme programming25 min de lectura

Feature Toggles. Cómo implementarlas de 0 a Experto

Por Sergio Perea #version control#extreme programming
Feature Toggles. Cómo implementarlas de 0 a Experto

Las Feature Toggles (también conocidas como Feature Flags) son una técnica de desarrollo que permite activar o desactivar funcionalidades en un sistema sin necesidad de modificar el código ni desplegar una nueva versión. En esencia, ambas expresiones son sinónimos y se utilizan de forma intercambiable; a veces también se habla de feature bits o feature flippers.

La idea principal consiste en introducir en el código “puntos de decisión” que, en función de la configuración del flag, ejecutan una ruta u otra del programa.

Esto posibilita que equipos de desarrollo trabajen con nuevas funcionalidades de manera controlada, reduciendo riesgos y favoreciendo prácticas como la Integración Continua y la Entrega Continua.

Por ejemplo, un equipo puede estar desarrollando un nuevo algoritmo que tardará semanas en estar listo. En lugar de mantener el trabajo en una rama aparte y sufrir los problemas de fusiones largas y conflictivas, los desarrolladores integran el código en la rama principal desde el inicio, pero protegido por una feature toggle. De esta forma, la funcionalidad no afecta al resto del sistema hasta que se decida activarla.

Esta estrategia es genial para proyectos con despliegues frecuentes y rápidos. Por ello te aconsejo leerte mi artículo sobre TBD, que es un caso de uso ideal para implantar estas Feature Toggles.

Por cierto: el uso de feature toggles no se limita a facilitar el trabajo técnico. También permiten escenarios como:

  • Liberación progresiva (canary releases), activando una nueva función solo para un pequeño porcentaje de usuarios antes de lanzarla a todo el mundo. Lo veremos en este artículo.

  • Pruebas A/B o experimentos, mostrando diferentes versiones de una funcionalidad a distintos grupos de usuarios para tomar decisiones basadas en datos.

  • Control operativo, habilitando o deshabilitando componentes que consumen muchos recursos en situaciones de carga elevada.

  • Gestión de permisos, ofreciendo funciones premium a ciertos clientes o permitiendo que usuarios internos prueben características antes de su lanzamiento público.

Sin embargo, estas ventajas vendrán acompañadas de complejidad adicional: las toggles multiplican el número de posibles rutas de ejecución y estados del sistema, lo que complica la validación y las pruebas. Por eso, es fundamental clasificarlas, gestionarlas con herramientas adecuadas y, sobre todo, retirarlas cuando dejan de ser necesarias.

Al final, lo importante es entender que las Feature Toggles son un mecanismo flexible y potente para separar el momento en que se despliega el código del momento en que se libera la funcionalidad. Es el mejor caso de uso que tienen.

En este artículo te contaré como implementar las Feature Toggles en tu proyecto, pero antes, debemos conocer los tipos de toggles.

Tipos de Toggles que debes conocer

Aunque todas las feature toggles comparten el mismo principio (activar o desactivar funcionalidades sin necesidad de desplegar nuevo código), no todas cumplen la misma función ni tienen la misma vida útil.

Podemos distinguir varios tipos, cada uno con un propósito específico dentro del ciclo de vida del software.

Release Toggles (Toggles de Liberación)

Son las más comunes y probablemente las primeras que cualquier equipo implementa.

Imagina que estás construyendo una nueva funcionalidad compleja que tardará semanas en completarse. No quieres mantenerla en una rama aislada, porque sabes que las integraciones largas suelen ser dolorosas. La solución: proteger el nuevo código tras una toggle y mantenerlo “apagado” hasta que esté listo.

Este tipo de flag permite que el equipo siga desplegando a producción con total tranquilidad, aunque la funcionalidad aún no esté terminada. En otras palabras: separa el despliegue del lanzamiento.

Eso sí, tienen fecha de caducidad muy corta: en cuanto la nueva funcionalidad esté consolidada, la toggle debe retirarse. Así que requieren un buen mantenimiento.

Experiment Toggles (Toggles de Experimento o A/B Testing)

Aquí entramos en el terreno del producto y la toma de decisiones basada en datos. Estas toggles permiten mostrar diferentes versiones de una funcionalidad a distintos grupos de usuarios, de forma controlada y consistente.

Un ejemplo clásico es el de un botón de compra:

¿funciona mejor el texto “Comprar ahora” o “Añadir al carrito”?

Con una experiment toggle podemos dividir el tráfico en cohortes y comparar resultados reales. La clave está en que la decisión de qué camino tomar se hace en tiempo de ejecución y por usuario, garantizando que cada persona siempre vea la misma variante durante todo el experimento.

Estas toggles viven lo suficiente como para obtener datos estadísticamente significativos (días o semanas), pero deben retirarse una vez tomada la decisión.

Ops Toggles (Toggles Operacionales)

Si las anteriores estaban pensadas para el desarrollo y el producto, estas se diseñan para el mundo real de la operación.

Un ops toggle funciona como un interruptor de emergencia que los equipos de operaciones o SRE pueden accionar en producción para proteger la estabilidad del sistema.

Por ejemplo:

Un panel de recomendaciones que es muy costoso de calcular.

Bajo condiciones normales, está activo. Pero si la web empieza a sufrir una carga inesperada, se puede “apagar” de inmediato para reducir consumo de recursos sin necesidad de hacer un despliegue.

Algunas de estas toggles son temporales —hasta ganar confianza en un nuevo componente—, mientras que otras se convierten en auténticos “kill switches” permanentes, siempre listos para ser activados si algo se tuerce.

Permissioning Toggles (Toggles de Permisos o Segmentación)

Por último, tenemos las toggles orientadas a gestionar quién puede ver qué dentro de un sistema. Aquí hablamos de activar funcionalidades solo para ciertos grupos: usuarios premium, clientes enterprise, testers internos o un grupo reducido de “beta users”.

Este tipo de toggle no suele tener una fecha de caducidad corta. De hecho, muchas veces se quedan en el sistema durante años, porque forman parte de la estrategia de negocio: ofrecer experiencias distintas según el perfil del cliente. Son muy dinámicas, ya que la decisión depende del usuario que hace la petición.

Un ejemplo típico sería un SaaS que ofrece informes avanzados solo a clientes de pago. La funcionalidad existe para todos, pero solo quienes cumplen los criterios ven la opción disponible en su interfaz.

Gestión de Feature Toggles con un ejemplo en Python y Fastapi

Veamos cómo montar una API usando FastAPI para manejar rutas cuyos comportamientos cambian según feature flags.

FastAPI es un framework moderno, de alto rendimiento para crear APIs en Python 3.8+, con validación de datos, documentación automática y tipado estándar gracias a Pydantic.

Me gusta utilizarlo porque es muy liviano, y es fácil de implementar buenas prácticas con él.

1. Instalación de FastAPI y Uvicorn

pip install fastapi uvicorn pytest

2. Implementación simple de un gestor de flags

Podemos simular la obtención de feature flags con una clase sencilla, que podría eventualmente conectarse a una base de datos o sistema externo.

En realidad hay incluso servicios Saas que lo proporcionan (como Unleash), pero creo que aprenderás más sobre los feature flags si implementas un gestor desde cero, que además es potente para la mayoría de proyectos y te libero de una dependencia externa más.

Un “gestor de flags” no es más que una clase que guarda el estado de tus feature flags (por ejemplo, nueva_home = True/False) y ofrece métodos para:

  • Consultar si una flag está activa.
  • Cambiar su valor.
  • (Opcional) Evaluarla con contexto (por usuario, por porcentaje, por entorno, etc.). Esto ya depende del tipo de Feature Flag que hemos visto antes.
  • (Opcional) Guardar y cargar flags de un archivo JSON para que persistan. Lo haremos en este artículo.

Empezaremos con algo muy simple (lo alojaremos en memoria) y luego le añadimos “superpoderes” a todo esto paso a paso.

Vamos a tratar de crear una clase que cumpla los siguientes test:

# test_feature_flags.py import pytest from feature_flags import FeatureFlagManager # ----- Fixtures ----- @pytest.fixture def ff_empty(): """Gestor sin flags iniciales (aislado por test).""" return FeatureFlagManager() @pytest.fixture def ff_preloaded(): """Gestor con flags de ejemplo ya cargadas.""" return FeatureFlagManager({ "nueva_home": True, "checkout": False, "beta_banner": True, }) # ----- Tests básicos con fixtures ----- def test_init_without_flags(ff_empty): assert ff_empty.all_flags() == {} assert ff_empty.is_enabled("no_existe") is False # por defecto False def test_init_with_flags(ff_preloaded): assert ff_preloaded.is_enabled("nueva_home") is True assert ff_preloaded.is_enabled("checkout") is False assert ff_preloaded.is_enabled("beta_banner") is True def test_set_flag_and_get(ff_empty): ff_empty.set_flag("modo_dark", True) assert ff_empty.is_enabled("modo_dark") is True ff_empty.set_flag("modo_dark", False) assert ff_empty.is_enabled("modo_dark") is False def test_all_flags_returns_copy(ff_preloaded): flags_copy = ff_preloaded.all_flags() flags_copy["nueva_home"] = False # Cambiar la copia NO debe afectar al estado interno assert ff_preloaded.is_enabled("nueva_home") is True def test_set_flag_invalid_value(ff_empty): with pytest.raises(ValueError): ff_empty.set_flag("invalida", "si") # debe ser bool @pytest.mark.parametrize( "name,value,expected", [ ("a", True, True), ("a", False, False), ("b", True, True), ], ) def test_parametrized_set_and_read(ff_empty, name, value, expected): ff_empty.set_flag(name, value) assert ff_empty.is_enabled(name) is expected @pytest.mark.parametrize("missing_flag", ["x", "y", "z"]) def test_missing_flags_default_false(ff_empty, missing_flag): assert ff_empty.is_enabled(missing_flag) is False

El código resultante sería tan sencillo como este:

# feature_flags.py from typing import Dict class FeatureFlagManager: """ Gestor sencillo de feature flags en memoria. Guarda pares nombre -> bool. """ def __init__(self, initial_flags: Dict[str, bool] | None = None): self._flags: Dict[str, bool] = initial_flags.copy() if initial_flags else {} def is_enabled(self, name: str) -> bool: """¿Está activada la flag? Si no existe, devolvemos False por defecto.""" return self._flags.get(name, False) def set_flag(self, name: str, value: bool) -> None: """Crea o actualiza el valor de una flag.""" if not isinstance(value, bool): raise ValueError("El valor de la flag debe ser True o False (bool).") self._flags[name] = value def all_flags(self) -> Dict[str, bool]: """Devuelve un diccionario con TODAS las flags (copia).""" return self._flags.copy()

Para usarlo:

ff = FeatureFlagManager({"nueva_home": True}) print(ff.is_enabled("nueva_home")) # True ff.set_flag("nueva_home", False) print(ff.is_enabled("nueva_home")) # False print(ff.is_enabled("no_existe")) # False (por defecto)

Con esta clase tan simple ya puedes condicionar comportamientos en el resto de tu código

if ff.is_enabled("nueva_home"): mostrar_home_nueva() else: mostrar_home_antigua()

Vamos a ponerle persistencia

A veces quieres que tus flags sobrevivan si reinicias la app. Para eso podemos cargar/guardar en un JSON para mantener esos valores.

Para ello tenemos que ampliar el código escrito anteriormente. Nada del otro mundo. Sólo añadirle esa persistencia en un JSON.

Para ello, reescribimos los tests que deberían ser satisfechos por la clase:

# test_feature_flags_persistencia.py import json import pytest from pathlib import Path from feature_flags import FeatureFlagManager # ---------------------------- # Fixtures de apoyo # ---------------------------- @pytest.fixture def flags_file(tmp_path: Path) -> Path: """Devuelve la ruta a un archivo JSON de flags dentro de un directorio temporal.""" return tmp_path / "flags.json" @pytest.fixture def write_json(): """Helper para escribir JSON en un Path.""" def _write(path: Path, data: dict): path.write_text(json.dumps(data, indent=2), encoding="utf-8") return path return _write # ---------------------------- # Tests básicos en memoria # ---------------------------- def test_default_false_when_missing(): ff = FeatureFlagManager() assert ff.is_enabled("no_existe") is False def test_all_flags_returns_copy(): ff = FeatureFlagManager({"a": True}) copia = ff.all_flags() copia["a"] = False assert ff.is_enabled("a") is True # la copia no afecta al estado interno def test_set_flag_updates_and_requires_bool(): ff = FeatureFlagManager() ff.set_flag("modo_dark", True) assert ff.is_enabled("modo_dark") is True ff.set_flag("modo_dark", False) assert ff.is_enabled("modo_dark") is False with pytest.raises(ValueError): ff.set_flag("modo_dark", "si") # debe ser bool # ---------------------------- # Tests de inicialización con storage # ---------------------------- def test_init_without_existing_storage_keeps_initial(flags_file: Path): # No creamos el archivo: el gestor debe respetar initial_flags ff = FeatureFlagManager(initial_flags={"a": True, "b": False}, storage_path=str(flags_file)) assert ff.all_flags() == {"a": True, "b": False} # Y no debe crear el archivo hasta que guardemos algo assert not flags_file.exists() def test_init_with_existing_storage_overrides_initial(flags_file: Path, write_json): # El archivo existe y debe prevalecer sobre initial_flags en las claves coincidentes write_json(flags_file, {"a": False, "c": True}) ff = FeatureFlagManager(initial_flags={"a": True, "b": True}, storage_path=str(flags_file)) # Lo del disco gana para 'a'; 'c' se añade; 'b' permanece del initial (no estaba en disco) assert ff.all_flags() == {"a": False, "b": True, "c": True} def test_init_with_non_bool_values_are_ignored(flags_file: Path, write_json): # Valores no booleanos en el JSON deben ignorarse write_json(flags_file, {"ok": True, "mal": "si", "otro": 1, "lista": [True]}) ff = FeatureFlagManager(initial_flags={"base": False}, storage_path=str(flags_file)) assert ff.all_flags() == {"base": False, "ok": True} def test_init_with_invalid_json_raises(flags_file: Path): flags_file.write_text("{esto no es json", encoding="utf-8") with pytest.raises(json.JSONDecodeError): FeatureFlagManager(storage_path=str(flags_file)) # ---------------------------- # Tests de guardado en disco # ---------------------------- def test_set_flag_persists_to_disk(flags_file: Path): ff = FeatureFlagManager(storage_path=str(flags_file)) ff.set_flag("nueva_home", True) # Debe haberse creado el archivo y contener el valor assert flags_file.exists() data = json.loads(flags_file.read_text(encoding="utf-8")) assert data == {"nueva_home": True} # Cambiamos el valor y verificamos que se sobrescribe ff.set_flag("nueva_home", False) data = json.loads(flags_file.read_text(encoding="utf-8")) assert data == {"nueva_home": False} def test_save_is_noop_without_storage_path(tmp_path: Path): ff = FeatureFlagManager() ff.set_flag("x", True) # no debe intentar escribir nada en disco # Aseguramos que no hay archivos JSON creados en el directorio temporal # (no podemos saber el cwd, así que comprobamos que en tmp_path no haya nada) assert list(tmp_path.iterdir()) == [] def test_persist_and_reload_roundtrip(flags_file: Path): # Guardamos algo ff = FeatureFlagManager(storage_path=str(flags_file)) ff.set_flag("a", True) ff.set_flag("b", False) # Nueva instancia debe leer desde disco ff2 = FeatureFlagManager(storage_path=str(flags_file)) assert ff2.all_flags() == {"a": True, "b": False} assert ff2.is_enabled("a") is True assert ff2.is_enabled("b") is False

Y finalmente la clase nos quedaría así:

import json from pathlib import Path from typing import Dict class FeatureFlagManager: def __init__(self, initial_flags: Dict[str, bool] | None = None, storage_path: str | None = None): self._flags: Dict[str, bool] = {} self._storage_path = Path(storage_path) if storage_path else None if initial_flags: self._flags.update(initial_flags) # Si hay archivo de storage, lo cargamos if self._storage_path and self._storage_path.exists(): self._load_from_disk() # -------- API pública -------- def is_enabled(self, name: str) -> bool: return self._flags.get(name, False) def set_flag(self, name: str, value: bool) -> None: if not isinstance(value, bool): raise ValueError("El valor de la flag debe ser True o False (bool).") self._flags[name] = value self._save_to_disk() def all_flags(self) -> Dict[str, bool]: return self._flags.copy() # -------- Persistencia -------- def _load_from_disk(self) -> None: data = json.loads(self._storage_path.read_text(encoding="utf-8")) # Validación simple: todo debe ser bool for k, v in data.items(): if isinstance(v, bool): self._flags[k] = v def _save_to_disk(self) -> None: if not self._storage_path: return self._storage_path.write_text(json.dumps(self._flags, indent=2), encoding="utf-8")

El archivo JSON tendría que tener esta estructura:

{ "nueva_home": true, "pago_experimental": false }

Y ahora vamos a ver como se podría usar.

ff = FeatureFlagManager(storage_path="flags.json") print(ff.all_flags()) # Carga lo que haya en flags.json ff.set_flag("pago_experimental", True) # Se guarda automáticamente en disco

Otro superpoder: flags con porcentaje de activación (rollout gradual)

Esto es muy útil para activar una función al X% de usuarios de forma estable (el mismo usuario siempre cae en el mismo lado). Para eso usamos un hash del user_id y lo convertimos a 0–99. Suena lioso, pero verás que es muy sencillo de hacer a partir de lo que ya tenemos. Como ya te he contado, este tipo de flag es importantísimo para realizar test A/B.

Idea

Cada flag puede ser:

  • bool → on/off global.
  • dict con una clave rollout (0–100) → porcentaje.

Regla de evaluación:

  1. Si es bool, devolvemos ese valor.
  2. Si tiene rollout, calculamos el “bucket” del usuario: bucket = hash(user_id) % 100.
  3. Está activada si bucket < rollout.

feature_flags.py (soporta bool o rollout por usuario)

import hashlib from typing import Dict, Union, TypedDict class RolloutConfig(TypedDict): rollout: int # 0..100 FlagValue = Union[bool, RolloutConfig] class FeatureFlagManager: def __init__(self, initial_flags: Dict[str, FlagValue] | None = None): self._flags: Dict[str, FlagValue] = initial_flags.copy() if initial_flags else {} def is_enabled(self, name: str, user_id: str | None = None) -> bool: value = self._flags.get(name, False) # Caso simple: bool if isinstance(value, bool): return value # Caso rollout if isinstance(value, dict) and "rollout" in value: if user_id is None: # Sin user_id no podemos hacer bucketing: asumimos False return False pct = value["rollout"] if not (0 <= pct <= 100): return False bucket = self._stable_bucket(user_id) return bucket < pct # Valor inválido → False return False def set_flag(self, name: str, value: FlagValue) -> None: # Validación básica if isinstance(value, dict): pct = value.get("rollout") if not isinstance(pct, int) or not (0 <= pct <= 100): raise ValueError("rollout debe ser un entero entre 0 y 100.") elif not isinstance(value, bool): raise ValueError("La flag debe ser bool o dict con {'rollout': int}.") self._flags[name] = value def all_flags(self) -> Dict[str, FlagValue]: return self._flags.copy() @staticmethod def _stable_bucket(user_id: str) -> int: """Convierte user_id en un número estable 0..99 usando SHA-256.""" h = hashlib.sha256(user_id.encode("utf-8")).hexdigest() # Tomamos 8 hex chars (32 bits), lo pasamos a int y lo reducimos a 0..99 return int(h[:8], 16) % 100

Ejemplos:

ff = FeatureFlagManager({ "nueva_home": True, # bool "checkout_v2": {"rollout": 25} # 25% de usuarios }) print(ff.is_enabled("nueva_home")) # True print(ff.is_enabled("checkout_v2", user_id="user-123")) # True/False según bucket print(ff.is_enabled("checkout_v2")) # False (sin user_id)

Integración con FastAPI

Integramos el gestor en tu API para:

  • Consultar todas las flags.
  • Activar/desactivar una flag booleana.
  • Configurar un rollout.
  • Evaluar una flag para un user_id.

main.py

from fastapi import FastAPI, Depends, HTTPException from pydantic import BaseModel, Field from typing import Literal, Union, Dict from feature_flags import FeatureFlagManager # importa tu clase # ---- Instancia única del gestor (en memoria) ---- ff = FeatureFlagManager({ "nueva_home": True, "checkout_v2": {"rollout": 30} }) def get_manager() -> FeatureFlagManager: return ff # ---- Modelos de entrada/salida ---- class BoolFlagPayload(BaseModel): type: Literal["bool"] = "bool" value: bool class RolloutFlagPayload(BaseModel): type: Literal["rollout"] = "rollout" rollout: int = Field(ge=0, le=100) FlagPayload = Union[BoolFlagPayload, RolloutFlagPayload] class EvaluateQuery(BaseModel): user_id: str | None = None app = FastAPI(title="Feature Flags API") @app.get("/flags") def list_flags(manager: FeatureFlagManager = Depends(get_manager)) -> Dict[str, dict | bool]: return manager.all_flags() @app.put("/flags/{name}") def upsert_flag( name: str, payload: FlagPayload, manager: FeatureFlagManager = Depends(get_manager), ): try: if payload.type == "bool": manager.set_flag(name, payload.value) else: manager.set_flag(name, {"rollout": payload.rollout}) return {"ok": True, "flag": name, "value": manager.all_flags()[name]} except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @app.get("/flags/{name}/evaluate") def evaluate_flag( name: str, user_id: str | None = None, manager: FeatureFlagManager = Depends(get_manager), ): enabled = manager.is_enabled(name, user_id=user_id) return {"flag": name, "user_id": user_id, "enabled": enabled}

Lanza la API:

uvicorn main:app --reload

Puedes hacer algunas pruebas rápidas con curl para ver que funciona:

# Ver todas las flags curl http://localhost:8000/flags # Flag booleana ON curl -X PUT http://localhost:8000/flags/nueva_home \ -H "Content-Type: application/json" \ -d '{"type":"bool","value":true}' # Rollout al 50% curl -X PUT http://localhost:8000/flags/checkout_v2 \ -H "Content-Type: application/json" \ -d '{"type":"rollout","rollout":50}' # Evaluar para un usuario curl "http://localhost:8000/flags/checkout_v2/evaluate?user_id=alicia"

Creación de rutas condicionadas por flags en nuestra API

Imagínate que tienes este endpoint y quieres aplicarle una Feature Flag:

from fastapi import FastAPI, HTTPException, Depends app = FastAPI() ff_client = DummyFFClient({"test_feature": True}) def get_ff_client(): return ff_client @app.get("/funcionalidad") def funcionalidad_principal(ff: DummyFFClient = Depends(get_ff_client)): if ff.is_enabled("test_feature"): return {"mensaje": "Funcionalidad nueva activada"} raise HTTPException(status_code=404, detail="Funcionalidad no disponible")

Con FastAPI (que está basado en Starlette), lo más cómodo es usar functools.wraps para crear un decorador que reciba el nombre de la flag y, opcionalmente, datos del usuario. Dentro del decorador verificamos la flag usando el ff_client. Si la flag está deshabilitada → lanzamos un HTTPException(404).

# feature_decorators.py from functools import wraps from typing import Callable from fastapi import Depends, HTTPException from main import get_ff_client, DummyFFClient # importa tu cliente def require_flag(flag_name: str): """ Decorador para proteger endpoints con un feature flag. Si la flag está desactivada, devuelve un 404 automáticamente. """ def decorator(func: Callable): @wraps(func) def wrapper(*args, ff: DummyFFClient = Depends(get_ff_client), **kwargs): if not ff.is_enabled(flag_name): raise HTTPException(status_code=404, detail=f"Funcionalidad '{flag_name}' no disponible") return func(*args, ff=ff, **kwargs) return wrapper return decorator

Y en la API se implementaría así:

from fastapi import FastAPI from feature_flags import FeatureFlagManager from feature_decorators import require_flag app = FastAPI() ff_client = FeatureFlagManager({"test_feature": True}) def get_ff_client(): return ff_client @app.get("/funcionalidad") @require_flag("test_feature") # 👈 aquí usamos la fixture/decorador def funcionalidad_principal(ff: FeatureFlagManager = Depends(get_ff_client)): return {"mensaje": "Funcionalidad nueva activada"}

Buenas prácticas con Feature Flags

Cuando empieces a usar feature flags en tus proyectos, verás que al principio es muy fácil: un if que activa o no una función. Pero en aplicaciones reales los flags pueden crecer rápido y, si no los manejas bien, se vuelven un lío.
Aquí tienes una guía de buenas prácticas explicada paso a paso:

Usa un identificador estable para los usuarios

Si vas a activar un flag solo para un porcentaje de usuarios, necesitas que siempre les toque el mismo resultado.
No uses cosas que cambian todo el tiempo como el número de sesión o un timestamp.
Sí usa un identificador único y fijo como el user_id o el correo electrónico.

Así, si activas una función al 30% de usuarios, siempre serán los mismos, y no cambiará cada vez que recarguen la página.

Empieza siempre con un valor seguro por defecto

Si el sistema de flags falla o alguien pregunta por un flag que no existe, lo mejor es que devuelva False (desactivado).
Esto evita mostrar una función que todavía no está lista o que puede romper el sistema.

Define reglas claras de evaluación

Cuando tengas varias condiciones (por ejemplo lista de usuarios permitidos, usuarios bloqueados, atributos como país o plan), sigue siempre el mismo orden:

  1. Deny (usuarios que nunca pueden entrar).
  2. Allow (usuarios que siempre entran).
  3. Atributos (ej. solo España o plan Pro).
  4. Rollout (porcentaje de activación).

Esto te da consistencia y evita resultados confusos.

Documenta cada flag

Pon nombres claros y explica para qué sirve el flag.
Ejemplo malo:

"flag1": True

Ejemplo bueno:

"checkout_v2": {"rollout": 25} # Nuevo proceso de compra

Esto ayuda a ti y a tus compañeros a entender rápidamente qué hace cada flag.

Limpia los flags que ya no se usen

Un error muy común es dejar los flags viejos en el código después de que la nueva función ya está activa para todos. Eso se llama deuda técnica: basura acumulada que hace el código más difícil de mantener.
👉 Regla de oro: cuando un flag ya llegó al 100% y la función es estable, borra el flag y el código viejo.

Separa flags por entorno

Tu aplicación seguramente tendrá entornos distintos: desarrollo, staging, producción.
No uses las mismas flags para todos, porque lo que pruebas en desarrollo puede colarse en producción.
👉 Solución: tener flags separados por entorno o añadir un prefijo (ej. dev__checkout_v2, prod__checkout_v2).


Guarda registros (logging)

Cuando un flag cambia de valor o cuando un usuario entra o no a una función, puede ser útil guardar un log.
Esto sirve para:

  • Depurar errores (“¿por qué no se activó para este usuario?”).
  • Hacer auditoría (“quién tenía acceso a qué y cuándo”).

No abuses de los flags

Los flags son muy útiles, pero demasiados flags al mismo tiempo pueden complicar mucho el código.
Intenta que cada flag tenga una vida corta: lo usas para probar una función, lo despliegas poco a poco y luego lo quitas.

Con estas prácticas, incluso si estás empezando, podrás manejar feature flags de forma ordenada, segura y sin que tu código se convierta en un laberinto.

No utilizar feature toggles para lógica de negocio

Perfecto, Sergio 👍. Aquí tienes un apartado redactado en tono claro y profesional para tu artículo:


No utilizar feature toggles para lógica de negocio

Imagina que en una tienda online decides calcular el precio de dos maneras diferentes según si un toggle está en true o en false. Eso significa que tu regla de negocio (cómo se calcula el precio) depende de un interruptor. El problema es que:

  • Tu código se vuelve más difícil de leer y entender.
  • Es más probable que olvides apagar el toggle y tu aplicación termine con dos lógicas conviviendo mucho tiempo.
  • Al final, tu lógica de negocio queda “escondida” detrás de condicionales en lugar de estar clara y bien definida.

👉 Lo correcto es que la lógica de negocio siempre esté dentro del dominio, escrita de forma clara y estable.
👉 Los toggles deberían usarse solo para controlar si una nueva funcionalidad se muestra o no, o para hacer pruebas temporales.

Así que usa los feature toggles para exponer o probar funciones, no para definir cómo funciona tu negocio.