## Feature flags: enviar codigo incompleto a master sin romperlo Doctrina oficial de **trunk-based development**: master siempre desplegable. Cuando una feature no cabe en una sola rama corta, o cuando hay WIP que no esta terminado pero el resto si, **el codigo viaja detras de un flag OFF**. Asi master sigue verde y el codigo a medio terminar no llega a usuarios reales. Refs: [trunkbaseddevelopment.com/feature-flags/](https://trunkbaseddevelopment.com/feature-flags/), [trunkbaseddevelopment.com/branch-by-abstraction/](https://trunkbaseddevelopment.com/branch-by-abstraction/). ### Cuando usar feature flag | Situacion | Accion | |---|---| | Feature multi-issue (`0015a`, `0015b`, `0015c`) que llevan dias | Cada sub-issue mergea con flag OFF. Ultimo sub-issue activa flag. | | Refactor grande tipo "Branch by Abstraction" (ej. cambiar driver DB) | Crear abstraccion + impl nueva con flag. Eliminar antigua + flag al final. | | Cambio con riesgo en produccion que necesita rollback rapido | Flag para apagar sin redeploy. | | Despliegue gradual (un PC primero, luego todos) | Flag por PC/usuario/grupo. | | WIP detectado al cerrar otra rama | Envolver el codigo a medias en flag OFF, mergear, terminar despues. | ### Cuando NO usar feature flag - Bug fix autocontenido → mergear directo, sin flag. - Refactor que cabe en una rama corta → directo. - Docs, comments, type signatures → directo. - Codigo que no compila o no pasa tests → **NO viaja a master, ni con flag**. Flag protege codigo terminado, no roto. ### Flag != WIP - **WIP**: codigo a medias, no compila o no testea. NO va a master. - **Flag**: codigo terminado y testeado, pero no expuesto al usuario. SI va a master. Si hay 80% terminado y 20% pendiente: completar al menos un slice vertical funcional (compila, pasa tests, se puede activar end-to-end), mergear con flag OFF, dejar el 20% para otra rama. NO mergear el 20% sin proteger. ### Archivo de flags `dev/feature_flags.json` en la raiz del repo (registry o app). Formato canonico: ```json { "flags": { "": { "enabled": false, "issue": "0063", "description": "Descripcion 1 linea de la feature", "added": "2026-05-08", "enabled_at": null } } } ``` Cuando se activa: cambiar `enabled: true` y rellenar `enabled_at` con fecha. Cuando la feature ya es estable y no necesita rollback (semanas/meses despues): borrar el flag y todas sus ramas condicionales del codigo. **Los flags caducan**; documentar fecha de revision para evitar que se acumulen. ### Patron por stack #### Go (apps/services) Cargar flags al arrancar. Patron simple — hashmap en memoria + helper `Enabled(name)`: ```go // pkg/flags/flags.go (puro hasta donde se pueda) package flags import ( _ "embed" "encoding/json" ) type Flag struct { Enabled bool `json:"enabled"` Issue string `json:"issue"` Description string `json:"description"` } type Flags struct{ Flags map[string]Flag `json:"flags"` } func Parse(b []byte) (Flags, error) { var f Flags err := json.Unmarshal(b, &f) return f, err } func (f Flags) Enabled(name string) bool { flag, ok := f.Flags[name] return ok && flag.Enabled } ``` Uso: ```go if flags.Enabled("kanban-stickers") { registerStickerRoutes(router) } ``` Para flags en frontend embebido: serializar a `/api/flags` y leer desde el cliente (ver TS). #### TypeScript / React Inyectar en build (Vite) o exponer endpoint `/api/flags`: ```ts // src/flags.ts let cache: Record | null = null; export async function loadFlags(): Promise> { if (cache) return cache; const res = await fetch("/api/flags"); const data = await res.json(); cache = Object.fromEntries(Object.entries(data.flags).map(([k, v]: [string, any]) => [k, !!v.enabled])); return cache; } export function isEnabled(name: string): boolean { return !!(cache?.[name]); } ``` Render condicional: ```tsx {isEnabled("kanban-stickers") && } ``` Para flags en build-time (constantes del bundle), usar `import.meta.env.VITE_FLAG_X` o un plugin Vite que reemplace simbolos. #### Bash / pipelines Lectura directa con `jq`: ```bash ENABLED=$(jq -r '.flags["my-feature"].enabled' dev/feature_flags.json) if [ "$ENABLED" = "true" ]; then run_new_path else run_legacy_path fi ``` #### Python ```python import json from pathlib import Path def flags() -> dict: return json.loads(Path("dev/feature_flags.json").read_text())["flags"] def enabled(name: str) -> bool: f = flags().get(name) return bool(f and f.get("enabled")) if enabled("nuevo-pipeline"): run_new() else: run_legacy() ``` ### Branch by Abstraction (caso especial) Para cambios grandes (ej. swap iBatis → Hibernate, swap libreria, swap protocolo): 1. **Abstraer**: crear interfaz que envuelve la implementacion antigua. Master sigue verde con la antigua. Mergear. 2. **Implementar nueva**: bajo la misma interfaz, detras de flag OFF. Tests para ambas. Mergear. 3. **Activar**: flip flag a ON en commit pequeño. Si rompe, flip OFF de inmediato. 4. **Eliminar antigua**: borrar codigo legacy + flag + abstraccion. Mergear. Cada paso es un merge corto, master nunca esta roto, hay rollback en cada punto. ### Reglas operativas - **Un flag = un proposito**. Si necesitas dos toggles independientes, usa dos flags. - **Flag bool por defecto**. Si necesitas A/B/C, sigue siendo bool por nombre (`my-feature-v2`, `my-feature-v3`). - **Tests con flag ON y OFF**. CI corre ambos paths cuando el flag toca codigo critico. - **Documenta en el issue**: que flag protege que codigo, cuando se va a activar, cuando se va a borrar. - **No anidar flags**. Si una rama esta detras de dos flags, simplifica. - **Borra el flag**. Cuando la feature lleva semanas activa sin rollback, eliminar el flag es trabajo real, no opcional. ### Anti-patrones | Anti-patron | Por que es malo | |---|---| | `if (flag) { ... } else { ... }` esparcido por 30 archivos | Imposible de borrar. Usar inyeccion / strategy pattern. | | Flag que lleva 6 meses ON sin borrar | Deuda tecnica. Borrar el flag y simplificar. | | Flag para WIP que no compila | Master roto. Eso no es flag, es WIP — no debe estar en master. | | Flag condicional sobre tipos / esquemas DB | Migrations son irreversibles. No se "apaga" una columna. Usar branch-by-abstraction sobre la lectura/escritura, no sobre el schema. | | Flag con nombre del autor o del issue (`lucas-experiment`, `flag-0063`) | Sin contexto al releerlo. Nombrarlo por la feature: `kanban-stickers`. | ### Comandos relacionados - `/git-branch` — crea rama desde master. - `/git-push` — merge --no-ff + push. - Para registrar / activar un flag: editar `dev/feature_flags.json` directamente y commitear con el codigo correspondiente. No hay CLI dedicada todavia.