wandres.dev
ESTADO A ESCALA · equipos y dominios

Gobernar el crecimiento: medir el estado, revisarlo y podar lo muerto

El estado de una aplicación solo crece, y crece porque añadir es una operación local y barata mientras que retirar exige demostrar una ausencia, que es la clase de prueba más cara que existe. Esta lección convierte esa asimetría en un problema tratable: define métricas de tamaño y acoplamiento que se pueden extraer del propio repositorio, propone una revisión periódica con criterios de decisión, y describe un procedimiento de poda seguro basado en evidencia de uso real.

⏱ 18 min

Ninguna base de código adelgaza por sí sola. El estado crece porque cada adición resuelve un problema visible hoy y su coste se difiere a un futuro sin responsable, mientras que cada retirada exige demostrar que nada depende de lo que se elimina, y demostrar una ausencia es epistemológicamente caro incluso cuando el sistema es pequeño. La consecuencia es una acumulación monótona que nadie decidió y que solo se percibe cuando ya duele: campos que ningún código lee, ámbitos compartidos que sobrevivieron a la feature que los justificaba, cachés persistidas con formas de hace tres versiones. Gobernar ese crecimiento no consiste en tener más disciplina, sino en instituir un procedimiento que haga la poda tan barata y tan rutinaria como la adición.

🎯 Al terminar esta lección sabrás
  • Definir métricas de tamaño y acoplamiento del estado extraíbles del propio repositorio.
  • Instaurar una revisión periódica con criterios de decisión explícitos y acotada en tiempo.
  • Retirar estado con seguridad mediante evidencia de uso real y no por inspección.
  • Prevenir la reacumulación fijando presupuestos y caducidad para lo que se añade.

Medir antes de opinar

Una conversación sobre si el estado se ha vuelto inmanejable sin números es una conversación sobre sensaciones, y termina siempre en el mismo empate entre quien percibe complejidad y quien recuerda por qué cada pieza está ahí. Las métricas útiles son pocas y todas se obtienen del repositorio sin instrumentar nada, lo que importa porque una medición que exige montar infraestructura no se repite y una métrica que no se repite no sirve para detectar tendencias, que es exactamente lo único que aquí interesa.

📐

Superficie

Número de campos observables y de operaciones exportadas por dominio. Mide cuánto hay que entender para usarlo sin equivocarse.

🕸️

Alcance

Cuántos módulos distintos leen cada porción. Un alcance alto convierte cualquier cambio de forma en una migración coordinada.

✍️

Escritores

Cuántos sitios distintos escriben el mismo dato. Más de dos o tres suele indicar que faltó una operación con nombre de intención.

🪦

Edad sin lectura

Meses desde la última lectura observada en producción. La métrica más accionable y la que casi nadie recoge.

Una precisión sobre qué se está midiendo exactamente, porque la ambigüedad aquí arruina la serie. El tamaño relevante no es el número de bytes que el estado ocupa en memoria, que es casi siempre irrelevante y además fácil de confundir con un problema de rendimiento, sino el número de posiciones independientes que el sistema puede ocupar y que alguien tendría que considerar para razonar sobre él. Diez campos booleanos independientes producen mil veinticuatro configuraciones posibles; un único campo con cuatro valores excluyentes produce cuatro. La métrica útil, por tanto, penaliza la independencia entre campos y premia las representaciones que la eliminan.

De las cuatro, la relación entre escritores y lectores es la que mejor predice defectos futuros. Un dato con muchos lectores y un solo escritor es un caso sano: la forma puede cambiar detrás de una lectura derivada y la coherencia está garantizada por construcción. Un dato con varios escritores repartidos por módulos distintos es una condición de carrera latente y una fuente de estados imposibles, porque ninguna de las escrituras conoce las invariantes que las otras suponen. Cuando esa cifra aparece en una revisión, el remedio no es documentar sino cerrar la escritura tras una operación única del dominio propietario.

// tres senales extraibles del propio repositorio
// 1. superficie: exportaciones por dominio
//    rg "^export (const|function|type)" src/dominios/*/index.ts | contar por carpeta
// 2. alcance: modulos que importan cada dominio
//    rg "from \"@app/dominios/carrito\"" -l | contar
// 3. escritores: llamadas a operaciones de escritura por dominio
//    rg "carrito\\.(set|anadir|quitar|vaciar)" -l | contar

Las métricas de estado comparten con todas las demás una patología conocida, y conviene anticiparla: en cuanto se convierten en objetivo dejan de medir lo que medían. Un equipo evaluado por el número de exportaciones de sus dominios reducirá ese número agrupando cosas en objetos más anchos, que es exactamente el movimiento contrario al que se pretendía inducir. La protección es usarlas siempre como disparador de conversación y nunca como criterio de evaluación: la métrica señala dónde mirar, y quien mira decide, con contexto, si lo que encuentra es un problema o una consecuencia razonable del dominio.

ℹ️
La tendencia importa más que el valor absoluto

No existe un número correcto de campos observables, y buscarlo distrae. Lo informativo es la derivada: un dominio que pasó de doce a cuarenta exportaciones en dos trimestres está absorbiendo responsabilidades que no le corresponden, aunque cuarenta pueda ser un tamaño perfectamente razonable en otro contexto. Registra las cifras cada trimestre en un archivo del repositorio y la serie temporal dirá lo que ninguna instantánea puede decir.

La revisión periódica

Una revisión de estado es una reunión corta y regular con un guion fijo, y su valor no está en las decisiones que toma sino en que crea el único momento del calendario en el que retirar algo es una acción esperada en vez de una interrupción. Sin ese momento, la poda compite con el trabajo de producto y pierde siempre, porque su beneficio es difuso y su riesgo concreto, que es la asimetría que hace que ninguna base de código adelgace por iniciativa espontánea.

flowchart LR
M[recoger metricas del repositorio] --> S[senalar los tres peores casos]
S --> D[decidir podar migrar o mantener]
D --> P[marcar como obsoleto y observar uso real]
P --> R[retirar cuando no haya lecturas]
R --> M

El guion que funciona tiene cuatro pasos y cabe en cuarenta y cinco minutos. Se leen las métricas y su variación desde la revisión anterior. Se señalan los tres peores casos por alcance, por número de escritores o por edad sin lectura, y solo tres, porque una lista larga garantiza que no se hará nada. Se decide para cada uno entre podar, migrar o mantener con justificación registrada. Y se asigna responsable y fecha, porque una decisión sin ambas cosas es una opinión con acta.

// registro de decisiones junto al codigo, no en una herramienta aparte
// estado/REVISIONES.md
// 2026-Q2  bannerCerrado        -> podar     (sin lecturas desde 2025-11)  ana
// 2026-Q2  ultimoPedidoVisto    -> migrar    (pasa a parametro de ruta)    luis
// 2026-Q2  preferenciasUsuario  -> mantener  (alcance alto pero estable)   equipo

Dos detalles del procedimiento deciden si la revisión sobrevive más de tres trimestres. El primero es que las métricas se recojan de forma automática antes de la reunión y no durante ella, porque una sesión que empieza recopilando datos consume su energía en la parte mecánica y llega exhausta a la parte que exige juicio. El segundo es que la reunión tenga poder de decisión y no solo de recomendación: si la conclusión debe pasar por otra instancia para convertirse en trabajo planificado, el equipo aprenderá en dos iteraciones que la revisión no cambia nada y dejará de prepararla, con toda la razón.

// la recogida es automatica y se ejecuta antes de la revision
// package.json  ->  "estado:metricas": "node scripts/metricas-estado.mjs"
// salida: una fila por dominio con superficie, alcance, escritores y edad
// se compara contra la fila del trimestre anterior y se ordena por deterioro

La decisión de mantener merece tanta atención como la de podar, porque es la que más veces se toma y la que menos se justifica. Registrar el motivo cuesta una línea y produce un efecto acumulativo notable: en la revisión siguiente, quien lea que un ámbito se mantuvo por su estabilidad podrá comprobar si esa estabilidad se ha confirmado, y si no, la decisión anterior queda automáticamente refutada. Sin ese registro, cada revisión empieza de cero y el equipo discute cuatro veces al año exactamente lo mismo.

Podar con evidencia, no con inspección

Conviene además ordenar la poda por riesgo antes que por tamaño, porque el instinto de empezar por lo más grande es el que más incidentes produce. Lo primero que se retira es aquello cuya desaparición no puede romper ninguna invariante: campos de presentación, marcas de banners, restos de experimentos concluidos. Lo último, aquello de lo que dependen reglas de negocio, aunque parezca muerto. Ese orden tiene un beneficio adicional y decisivo: las primeras podas construyen la confianza del equipo en el procedimiento, y sin esa confianza nadie autorizará jamás las que de verdad importan.

Retirar estado por inspección del código es inseguro en cualquier lenguaje con acceso dinámico, con serialización o con datos persistidos en dispositivos ajenos, que es el caso de prácticamente toda aplicación real. El procedimiento seguro sustituye la inspección por observación: se marca la porción como obsoleta, se instrumenta su lectura, se espera un periodo que cubra los ciclos de uso reales, y se retira cuando el contador siga en cero. Es más lento que borrar y es la única forma de borrar sin generar un incidente.

// paso 1: marcar y observar en vez de borrar a ciegas
export const bannerCerrado = observar(estado.bannerCerrado, {
  alLeer: () => registrar("estado-obsoleto", { campo: "bannerCerrado" }),
})

// paso 2: tras un periodo sin lecturas, retirar el campo y su migracion
// paso 3: dejar una migracion que lo elimine del estado persistido antiguo

La poda tiene además una forma que casi nunca se reconoce como tal y que produce la mayor reducción por unidad de esfuerzo: sustituir varios campos independientes por una unión discriminada que solo admita las combinaciones válidas. No se elimina información, se elimina la posibilidad de representarla mal, y con ella desaparecen las ramas defensivas, las comprobaciones redundantes y los casos imposibles que alguien escribió por si acaso. Medido en posiciones alcanzables del estado, este cambio poda más que cualquier borrado de campos muertos, y a diferencia de aquel no requiere periodo de observación.

// cuatro campos independientes: dieciseis combinaciones, doce sin sentido
type Carga = { cargando: boolean; error?: Error; datos?: Pedido; vacio: boolean }

// una union discriminada: cuatro posiciones y ninguna invalida
type Carga =
  | { fase: "inicial" }
  | { fase: "cargando" }
  | { fase: "listo"; datos: Pedido }
  | { fase: "fallo"; error: Error }

El estado persistido añade una dificultad que se olvida con frecuencia: aunque el código nuevo ya no lo lea, sigue existiendo en los dispositivos de los usuarios y reaparecerá en cuanto alguien rehidrate una versión antigua. Por eso la poda no termina al borrar el campo del tipo, sino al añadir la migración que lo elimina del almacenamiento y al comprobar que la versión del esquema se incrementa. Omitir ese paso produce el fenómeno más desconcertante de todos, que es un dato que vuelve por sí solo semanas después de haberlo eliminado.

La instrumentación de lectura tiene un coste que conviene acotar desde el principio para que no se convierta en un problema propio. Registrar cada acceso a un campo caliente puede generar un volumen de telemetría desproporcionado, así que la práctica correcta es muestrear y quedarse solo con el primer registro por sesión, que es toda la información que la decisión necesita: no interesa cuántas veces se leyó, sino si alguien lo leyó. Y conviene poner fecha de retirada a la propia instrumentación, porque los observadores olvidados son también una forma de estado muerto.

// muestreo por sesion: la decision solo necesita saber si alguien leyo
let registrado = false
export const bannerCerrado = observar(estado.bannerCerrado, {
  alLeer: () => { if (!registrado) { registrado = true; registrar("obsoleto") } },
})
// retirar esta instrumentacion antes de 2026-12 aunque no se haya decidido nada
⚠️
Lo que parece muerto puede estar dormido

Una porción de estado sin lecturas durante seis semanas puede pertenecer a un flujo trimestral, a una campaña estacional o a un cliente que solo entra al cerrar el ejercicio. El periodo de observación debe cubrir el ciclo natural del negocio, no el del equipo de desarrollo, y ante la duda conviene medir un ciclo completo más un margen. Es lento y aburrido, y sigue siendo mucho más barato que restaurar un dato que alguien necesitaba una vez al año.

💡
Presupuesto y caducidad para lo nuevo

La poda sola no basta si la entrada no se regula. Dos mecanismos baratos lo consiguen: un presupuesto por dominio, de modo que añadir una exportación por encima del límite obligue a retirar otra, y una fecha de caducidad para todo estado creado por un experimento o una campaña, registrada en el momento de crearlo, cuando el contexto todavía existe y el coste de escribirla es una línea. Casi todo el estado muerto de cualquier proyecto nació con una vida útil conocida que nadie anotó.

Podar no es limpieza: es la única forma de conservar la capacidad de razonar

Hay una interpretación cómoda de la poda de estado según la cual se trata de higiene, de dejar el código ordenado, de una virtud profesional agradable pero secundaria frente al trabajo real de construir funcionalidad. Esa interpretación explica por qué la poda nunca se hace, porque ninguna organización sacrifica funcionalidad por estética. La formulación correcta es otra y es mucho más dura. La capacidad de un equipo para razonar sobre su sistema depende de que el modelo mental que las personas sostienen corresponda a lo que el sistema hace, y cada porción de estado muerta introduce una discrepancia permanente entre ambos: alguien la verá, supondrá que significa algo, y construirá sobre esa suposición. El coste no es la memoria que ocupa ni las líneas que suma, sino que degrada la fiabilidad de toda inferencia futura sobre esa región del código, y esa degradación se propaga, porque quien no puede confiar en lo que lee se protege duplicando en vez de reutilizando, lo que a su vez añade más estado. Vista así, la poda no compite con el trabajo de producto: es la condición que mantiene ese trabajo posible a coste constante en vez de creciente. Y explica también por qué debe ser periódica y no oportunista. Un sistema que solo se poda cuando duele acumula durante todo el intervalo entre dolores una deuda cuyo interés se paga en decisiones equivocadas que nadie atribuye a su causa, porque el error se comete meses después y a manos de otra persona. La revisión regular no elimina esa deuda mejor que una limpieza puntual; la elimina antes de que llegue a producir inferencias falsas, que es lo único que en este terreno resulta verdaderamente irreversible.

⚔️ Instaura una revisión y poda algo
  1. Extrae hoy las cuatro métricas de tu proyecto y guárdalas fechadas en un archivo del repositorio para tener una primera línea base.
  2. Localiza la porción de estado con más escritores distintos y cierra sus escrituras tras una única operación con nombre de intención.
  3. Marca como obsoleta la porción con más meses sin lectura aparente e instrumenta su lectura durante un ciclo completo de negocio.
  4. Retírala cuando el contador siga en cero, añadiendo la migración que la elimina del estado persistido y subiendo la versión del esquema.
  5. Fija un presupuesto de exportaciones por dominio y una fecha de caducidad obligatoria para todo estado nacido de un experimento.