Files
fn_registry/.claude/rules/feature_flags.md
T
egutierrez 48dd5a1869 docs(rules): TBD con feature flags para WIP sin romper master
- apps_tbd.md: tabla de decision para WIP en working tree (incluir/flag/stash/issue separado)
- feature_flags.md (nuevo): doctrina TBD oficial, patrones Go/TS/Bash/Py, branch-by-abstraction, anti-patrones
- INDEX: entrada 24

Refs: trunkbaseddevelopment.com/feature-flags y branch-by-abstraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:03:48 +02:00

6.7 KiB

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/, 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:

{
  "flags": {
    "<flag-name>": {
      "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):

// 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:

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:

// src/flags.ts
let cache: Record<string, boolean> | null = null;

export async function loadFlags(): Promise<Record<string, boolean>> {
	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:

{isEnabled("kanban-stickers") && <StickerToolbar ... />}

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:

ENABLED=$(jq -r '.flags["my-feature"].enabled' dev/feature_flags.json)
if [ "$ENABLED" = "true" ]; then
	run_new_path
else
	run_legacy_path
fi

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.