wandres.dev
ESTADO A ESCALA · equipos y dominios

Estado y arquitectura de módulos: el reparto dicta las dependencias

El grafo de paquetes de un proyecto no lo decide quien dibuja la arquitectura, sino quien decide dónde vive cada dato: toda porción de estado compartida por dos módulos crea entre ellos una dependencia que ningún diagrama puede deshacer. Esta lección examina cómo el reparto del estado determina la dirección de las dependencias, por qué produce ciclos y paquetes de contratos hipertrofiados, y qué técnicas de inversión permiten separar módulos que comparten información sin acoplarlos.

⏱ 19 min

Hay una relación que rara vez se enuncia y que gobierna la estructura física de todo proyecto grande: el grafo de dependencias entre paquetes es una consecuencia del reparto del estado, no una decisión independiente. Si dos módulos comparten un dato, uno de los tres tiene que ocurrir, y solo tres: o uno depende del otro, o ambos dependen de un tercero que lo posee, o el dato se duplica y alguien paga el coste de mantener las copias coherentes. No existe una cuarta opción, por muchas capas de abstracción que se interpongan, y por eso las discusiones sobre cómo dividir un repositorio en paquetes se atascan una y otra vez hasta que alguien plantea la pregunta anterior, que es quién posee cada dato.

🎯 Al terminar esta lección sabrás
  • Derivar la dirección de las dependencias entre módulos a partir de la propiedad del estado.
  • Reconocer los ciclos y el paquete de contratos hipertrofiado como síntomas del mismo error.
  • Aplicar inversión de dependencias para separar módulos que comparten información.
  • Evaluar el efecto del reparto sobre compilación incremental, pruebas y despliegue.

De la propiedad del dato a la flecha del grafo

La regla es mecánica. Quien posee un dato no depende de quien lo consume; quien lo consume depende de quien lo posee. En consecuencia, decidir que el carrito posee las líneas y que el checkout las lee equivale a declarar que el paquete de checkout depende del de carrito, y esa declaración tiene efectos que nadie eligió explícitamente: el orden de compilación, qué se recompila cuando algo cambia, qué se puede probar aisladamente y qué equipos quedan bloqueados esperando a otros. La arquitectura de módulos no se diseña en un diagrama, se descubre leyendo dónde vive cada dato.

⬇️

Dependencia directa

El consumidor importa la superficie del propietario. Simple y legible, aceptable cuando la dirección coincide con la jerarquía del producto.

🧩

Paquete de contratos

Ambos dependen de un tercero que solo define tipos y claves. Rompe el ciclo, pero se hipertrofia si empieza a acumular lógica.

🔄

Inversión por puerto

El propietario declara la interfaz que necesita y otro la satisface. Invierte la flecha sin duplicar datos ni crear ciclos.

📣

Notificación de hechos

Los módulos publican hechos ocurridos y quien quiera reacciona. Máximo desacoplamiento y mínima trazabilidad; úsalo con moderación.

La tercera opción de la tríada, duplicar el dato, se descarta con demasiada rapidez en las discusiones de arquitectura y merece un examen honesto, porque a veces es la correcta. Duplicar elimina la dependencia entre módulos a cambio de introducir un problema de coherencia, y ese intercambio es ventajoso cuando el dato cambia poco, cuando una divergencia temporal es tolerable y cuando la independencia comprada permite desplegar por separado. Es exactamente el razonamiento que sostiene la replicación en sistemas distribuidos, y su aplicación dentro de un mismo proceso obedece a la misma lógica: se paga coherencia para comprar autonomía, y la única respuesta equivocada es hacerlo sin saber que se está pagando.

El primer diagnóstico útil consiste en comprobar si la dirección de las dependencias coincide con la dirección de la estabilidad. Un módulo estable, del que muchos dependen y que rara vez cambia, puede permitirse ser dependido. Un módulo volátil que cambia cada semana no debería tener dependientes, y si los tiene es porque posee un dato que no le corresponde. Cuando esas dos direcciones se contradicen, cada cambio en el módulo volátil recompila y rompe medio proyecto, y el equipo interpreta el síntoma como lentitud de las herramientas cuando en realidad es un error de reparto.

// la propiedad decide la flecha, aunque nadie la haya dibujado
// paquete carrito  : posee las lineas
// paquete checkout : las lee  ->  checkout depende de carrito

// si ademas carrito lee el metodo de pago que posee checkout, hay ciclo
// y ningun refactor de carpetas lo arregla: hay que mover la propiedad

Comprobar esta correspondencia no requiere herramientas especiales: basta cruzar la lista de dependientes de cada paquete con su número de cambios por trimestre. Los casos problemáticos saltan de inmediato, y suelen ser dos o tres en cualquier proyecto. La utilidad de hacerlo explícito es que convierte una queja recurrente y difusa sobre la lentitud del entorno en una lista corta de decisiones concretas sobre dónde mover la propiedad de un dato, que es una conversación que sí se puede cerrar.

// cruzar volatilidad con numero de dependientes revela el error de reparto
// paquete        cambios por trimestre    dependientes
// contratos                 2                  14   <- estable y dependido, sano
// carrito                  38                   9   <- volatil y dependido, error
// checkout                 31                   0   <- volatil y hoja, sano
⚠️
Los ciclos no son un problema de importaciones

Cuando aparece una dependencia circular entre paquetes, la reacción habitual es mover un archivo, extraer un tipo o añadir una importación diferida para que el compilador calle. Todas esas maniobras ocultan el síntoma sin tocar la causa, que es siempre la misma: dos módulos se han repartido mal la propiedad de un dato y cada uno posee una parte de algo que es indivisible. El remedio es decidir cuál de los dos lo posee entero, o extraer un tercero que lo posea, pero nunca hacer que el grafo mienta.

El paquete de contratos y su hipertrofia

Hay un caso particular de esta contradicción que aparece en casi todos los proyectos y que se resuelve mal por costumbre: el módulo de sesión. Es estable, muchos dependen de él y todo apunta a que debe estar abajo en el grafo, pero al mismo tiempo necesita saber de permisos que dependen del dominio, de preferencias que dependen de la interfaz y de facturación que depende de un tercero. La solución no consiste en subirlo ni en bajarlo, sino en partirlo: la identidad, que es estable y no depende de nada, se queda abajo, y las políticas que se derivan de ella, que son volátiles, viven en los dominios que las necesitan.

Extraer un paquete común que contenga los tipos y las claves compartidas es la solución correcta a un problema real, y también el punto donde comienza el fallo estructural más extendido en los repositorios grandes. El paquete nace pequeño y honesto: unos cuantos tipos, unas claves de consulta, quizá una función de validación. Con el tiempo empieza a recibir todo aquello que dos módulos necesitan y ninguno quiere poseer, hasta que se convierte en el nodo del que depende absolutamente todo y que, por tanto, nadie puede modificar sin recompilar el mundo entero.

flowchart TD
C[paquete comun] --> H[crece con lo que nadie quiere poseer]
H --> T[todos dependen de el]
T --> R1[cualquier cambio recompila todo]
T --> R2[cualquier cambio exige coordinar equipos]
T --> R3[nadie puede probar un modulo aislado]

El criterio para mantenerlo sano es severo y sencillo: el paquete común puede contener tipos, constantes y funciones puras sin dependencias, y nada más. En cuanto acepta estado, acceso a red o lógica de dominio deja de ser un contrato y pasa a ser un módulo con dueños múltiples, que es la definición precisa de un bien sin propietario. La prueba es preguntar si el paquete podría publicarse por separado y ser útil sin el resto del sistema; si la respuesta exige explicaciones, ya se ha hipertrofiado.

// contrato legitimo: tipos, claves y funciones puras
export type PedidoId = string & { readonly marca: unique symbol }
export const clavePedido = (id: PedidoId) => ["pedidos", id] as const
export function esCancelable(estado: EstadoPedido): boolean { /* pura */ }

// hipertrofia: el contrato empieza a poseer estado y a hablar con la red
// export const storePedidos = crearStore(...)   <- ya no es un contrato
// export async function traerPedido(id) { ... } <- arrastra el cliente http

La versión más dañina de la hipertrofia no es la que acumula funciones sino la que acumula tipos con demasiada estructura. Un contrato que define la forma completa del pedido, con sus veinte campos y sus relaciones, obliga a que todos los consumidores conozcan esos veinte campos aunque usen tres, y convierte cualquier ampliación del modelo en un cambio que atraviesa el sistema entero. El remedio consiste en que el contrato exponga el mínimo que cada consumidor necesita, en varios tipos estrechos, en vez de un tipo ancho que todos comparten: es la segregación de interfaces aplicada a los datos, y su efecto sobre el grafo de recompilación es inmediato y medible.

// contrato ancho: todos conocen los veinte campos y recompilan con cada uno
export type Pedido = { id: string; lineas: Linea[]; /* ...dieciocho mas */ }

// contratos estrechos: cada consumidor depende solo de lo que usa
export type PedidoResumen = { id: PedidoId; total: number }
export type PedidoCancelable = { id: PedidoId; estado: EstadoPedido }

Un contrato bien acotado tiene además una propiedad valiosa que se pierde en cuanto crece: cambia muy poco. La estabilidad del nodo central es lo que hace tolerable que todos dependan de él, porque la recompilación masiva solo duele si ocurre a menudo. De modo que la métrica que conviene vigilar no es el tamaño del paquete común sino su frecuencia de cambio comparada con la del resto, y una relación cercana a uno indica que el contrato se ha convertido en el lugar donde se hace el trabajo.

Invertir la flecha sin duplicar el dato

Antes de invertir conviene comprobar que la dirección es realmente la equivocada y no simplemente incómoda. La inversión se justifica cuando la dependencia va de lo estable hacia lo volátil, cuando obliga a recompilar un módulo grande por un cambio en uno pequeño, o cuando impide probar una pieza sin arrancar la otra. No se justifica cuando el único motivo es que el diagrama quedaría más bonito con las flechas al revés, y esa distinción importa porque las inversiones innecesarias se acumulan igual que cualquier otra estructura y son mucho más difíciles de retirar que de introducir.

Cuando la dependencia que impone la propiedad del estado va en la dirección equivocada, la técnica clásica es la inversión: el módulo que necesita algo declara la forma de lo que necesita y otro se la proporciona desde fuera. Aplicada al estado, significa que un dominio no importa el store de otro sino que recibe una función de lectura que satisface una interfaz suya, y que la composición de ambos ocurre en el punto de arranque de la aplicación, que es el único lugar del sistema donde es legítimo conocerlo todo.

// el dominio declara lo que necesita, no de donde viene
export type FuentePermisos = { puede: (accion: string) => boolean }

export function crearDominioFacturas(deps: { permisos: FuentePermisos }) {
  const puedeEditar = derivar(() => deps.permisos.puede("editar-factura"))
  return { puedeEditar }
}

// la composicion ocurre solo en el arranque: alli si se conoce todo
const facturas = crearDominioFacturas({ permisos: sesion })

La segunda técnica de inversión es la notificación de hechos, y su relación con el estado es más delicada de lo que su popularidad sugiere. Publicar que algo ocurrió desacopla al emisor de todos sus receptores, lo cual es exactamente lo que se busca, pero también elimina del código cualquier rastro de quién reacciona a qué, lo cual destruye precisamente la propiedad que permite razonar sobre un sistema con estado. La regla práctica es reservarla para hechos con muchos interesados desconocidos y prohibirla para invariantes: nunca debe hacer falta que un mensaje llegue para que el sistema quede en un estado válido.

// aceptable: hecho con interesados desconocidos y sin invariante en juego
publicar({ tipo: "pedido-confirmado", id })   // analitica, notificaciones

// inaceptable: la coherencia del sistema depende de que alguien escuche
publicar({ tipo: "descontar-stock", sku })    // si nadie reacciona, invariante rota

La inversión tiene un coste que conviene admitir en vez de disimular: añade una indirección y hace que el recorrido de una dependencia exija abrir el punto de composición. Ese coste es aceptable cuando compra independencia real, es decir, cuando el módulo invertido puede compilarse, probarse y desplegarse sin el otro. Cuando no compra nada de eso, la indirección es puro ceremonial y conviene revertirla, porque una arquitectura con inversiones decorativas es más difícil de entender que una con dependencias directas y honestas.

💡
Mide el reparto por su efecto en el ciclo de trabajo

El reparto del estado se evalúa mejor con cuatro números que con cualquier diagrama: cuántos paquetes se recompilan al tocar el módulo más volátil, cuántos hay que arrancar para probar un dominio aislado, cuántos equipos deben coordinarse para cambiar un tipo compartido y cuánto tarda el ciclo desde el cambio hasta la comprobación. Si esos números empeoran trimestre a trimestre, el problema no está en las herramientas de compilación sino en dónde vive la información.

El grafo de módulos es la sombra que proyecta el reparto del estado

Los equipos dedican semanas a discutir la estructura de paquetes como si fuera un problema de organización de archivos que admite soluciones puramente formales, y la discusión resulta interminable porque se libra en el nivel equivocado. La estructura de módulos no es una variable libre: es la sombra que proyecta una decisión anterior sobre quién posee cada dato, y como toda sombra, no se puede corregir moviéndola, sino solo moviendo el objeto que la produce. Esto explica por qué los grandes esfuerzos de reorganización de repositorios fracasan con tanta regularidad. Se redibuja el grafo, se renombran las carpetas, se extraen paquetes nuevos, y a los seis meses han reaparecido los mismos ciclos en lugares distintos, porque el reparto del estado no cambió y el reparto del estado es lo que genera las flechas. La consecuencia metodológica es que cualquier trabajo serio de arquitectura debe empezar por un inventario de propiedad, dato por dato, respondiendo quién lo escribe, quién lo lee y quién responde de su forma, y solo después derivar de ahí el grafo de paquetes, que en ese punto ya no será una elección sino una lectura. Hay además una consecuencia más incómoda. Como la propiedad del estado también determina qué equipos deben coordinarse, la arquitectura de módulos y la estructura organizativa no son dos sistemas que convenga alinear, sino dos manifestaciones del mismo reparto. Cambiar uno sin el otro produce una tensión que siempre se resuelve del mismo modo: la realidad del código termina imponiéndose sobre el organigrama, porque los datos no obedecen a las jerarquías, y los equipos acaban comunicándose exactamente por donde el estado los obligó a hacerlo desde el principio.

⚔️ Deriva tu grafo desde la propiedad
  1. Construye un inventario de las porciones de estado de tu proyecto anotando quién las escribe, quién las lee y quién responde de su forma.
  2. Dibuja el grafo de dependencias que ese inventario implica y compáralo con el grafo real de tus paquetes.
  3. Localiza todos los ciclos y determina, para cada uno, cuál es el dato cuya propiedad está partida entre dos módulos.
  4. Audita tu paquete común: separa lo que es contrato puro de lo que se coló con estado o con red, y mide su frecuencia de cambio.
  5. Elige la dependencia más costosa en tiempo de recompilación e inviértela mediante un puerto, midiendo antes y después los cuatro números del ciclo de trabajo.