wandres.dev
ESTADO A ESCALA · equipos y dominios

Estado compartido: lo transversal de verdad y lo que solo lo parece

Cada vez que dos features necesitan el mismo dato aparece la tentación de promoverlo a global, y esa promoción es irreversible en la práctica porque nadie sabe después quién dependía de él. Esta lección propone un criterio para distinguir lo verdaderamente transversal de la coincidencia pasajera, un catálogo de alternativas a la promoción, y una disciplina de propiedad que impide que el estado compartido se convierta en el vertedero común del sistema.

⏱ 18 min

El estado compartido crece por un mecanismo que parece razonable en cada paso individual y es desastroso en el agregado. Dos features necesitan el mismo dato, alguien lo sube al ámbito común, y el coste marginal de esa decisión es casi nulo. La tercera feature lo encuentra ya disponible y lo usa sin preguntarse si le corresponde. La cuarta añade un campo. Dos años después existe un objeto que ninguna persona entiende completo, que nadie puede modificar sin miedo y que ningún equipo se considera responsable de mantener. No hubo un error de diseño: hubo doscientas decisiones locales correctas cuyo efecto acumulado nadie evaluó, porque promover es barato y despromover es prácticamente imposible.

🎯 Al terminar esta lección sabrás
  • Distinguir el estado genuinamente transversal de la coincidencia temporal entre dos features.
  • Aplicar alternativas a la promoción antes de subir un dato al ámbito común.
  • Asignar propiedad explícita a cada porción de estado compartido y su superficie mínima.
  • Reconocer los síntomas de un ámbito común que ha dejado de tener dueño.

El criterio: transversal por naturaleza, no por coincidencia

Que dos features usen el mismo dato no lo convierte en compartido, igual que dos personas que coinciden en un ascensor no forman un equipo. Lo verdaderamente transversal tiene tres marcas que se pueden comprobar de forma independiente. Primera, su ciclo de vida no coincide con el de ninguna feature: existe antes de que la primera se monte y sigue existiendo después de que la última se desmonte. Segunda, su cambio tiene consecuencias en regiones del producto que no se conocen entre sí. Tercera, no hay ninguna feature que pueda reclamar razonablemente su propiedad, porque cualquier candidata resultaría arbitraria.

🪪

Identidad y sesión

Quién es el usuario y qué puede hacer. Transversal indiscutible: precede a toda feature, sobrevive a todas y su cambio invalida decisiones en cualquier rincón.

🎨

Preferencias globales

Tema, idioma, densidad, zona horaria. Estables, de escritura rara y de lectura masiva; el caso más benigno del estado compartido.

🌐

Conectividad y capacidades

Estado de red, permisos del dispositivo, flags de funcionalidad. Nadie los posee porque describen el entorno, no el producto.

⚠️

Falsos transversales

El identificador del pedido en curso, el filtro del listado, el paso del asistente. Parecen comunes porque dos pantallas los miran, pero pertenecen a un flujo concreto.

Las tres marcas conviene comprobarlas por separado porque un dato puede cumplir una y fallar las otras dos, y esa combinación es precisamente la que engaña. El identificador del pedido en curso tiene una vida larga y aparece en muchas pantallas, lo que satisface la apariencia de la primera marca, pero falla la tercera de forma clara: el flujo de compra puede reclamar su propiedad sin ninguna arbitrariedad. La pregunta decisiva no es cuántos lo usan, sino si existe un candidato natural a poseerlo; cuando existe, el dato no es transversal, es de ese candidato, y compartirlo es un problema de superficie y no de ámbito.

El falso transversal más frecuente es el dato que dos features comparten porque una está a punto de navegar hacia la otra. Ahí no hay estado común: hay una transferencia. El remedio no es un contenedor global sino un canal explícito, ya sea un parámetro de ruta, una porción de la URL o un argumento de la operación que inicia el flujo. La diferencia práctica es enorme: la transferencia deja rastro en el punto de entrada de la feature receptora, mientras que el contenedor global hace que la dependencia sea invisible hasta que alguien la rompe.

El segundo falso transversal por frecuencia es el que nace de una restricción técnica y no de una necesidad del producto. Alguien necesita un dato en un punto del árbol donde no llega por composición, y en vez de reorganizar la composición lo sube al ámbito común, que siempre alcanza a todas partes. La promoción resuelve un problema de fontanería con una decisión de arquitectura, y el precio se paga después, cuando resulta que el dato tenía dueño legítimo y ahora lo poseen todos. Reconocer este caso es fácil por su firma característica: el dato se escribe en un único lugar, se lee en un único lugar, y sin embargo vive en el ámbito global.

// falso transversal: dos pantallas miran el mismo dato porque una lleva a la otra
estadoGlobal.pedidoSeleccionadoId = id      // dependencia invisible

// transferencia explicita: el dato viaja por el canal que ya existe
navegar(`/pedidos/${id}/detalle`)           // la ruta declara la dependencia
⚠️
La asimetría entre promover y despromover

Promover un dato al ámbito común cuesta una línea y no requiere permiso de nadie. Retirarlo exige demostrar que ningún consumidor lo lee, y esa demostración es imposible en cuanto el acceso ocurre a través de indirecciones, cadenas de texto o composición dinámica. Esa asimetría explica por qué el estado global solo crece: no es que a nadie le importe, es que la operación inversa no tiene un procedimiento seguro. Cualquier política sensata debe encarecer deliberadamente la promoción, porque es la única de las dos que todavía se puede regular.

Alternativas antes de promover

Entre la duplicación descontrolada y el contenedor global hay un espacio amplio que la mayoría de los equipos no explora, porque el debate se plantea como una disyuntiva y no como una escala. Recorrerla de menor a mayor alcance produce casi siempre una solución más barata que la global, y sobre todo produce una solución reversible, que es la propiedad que realmente importa cuando el sistema todavía va a cambiar mucho.

flowchart TD
N[dos features necesitan el mismo dato] --> A[elevar al ancestro comun mas cercano]
A --> B[si el ancestro esta lejos usar la URL o el servidor]
B --> C[si es dato remoto usar la cache de consultas]
C --> D[si nada encaja promover a global con dueno explicito]
D --> E[registrar la promocion y su justificacion]

La primera parada es el ancestro común más cercano, que suele estar mucho más abajo de lo que la intuición sugiere. La segunda es reconocer que muchos supuestos estados compartidos son en realidad estado de servidor consultado por dos sitios, y que una caché de consultas resuelve el problema sin crear ningún ámbito nuevo: las dos features piden lo mismo, la caché deduplica, y la propiedad del dato permanece donde siempre estuvo, en el servidor. La tercera es la URL, que es un contenedor compartido legítimo, inspeccionable, restaurable y con una semántica que todo el mundo entiende sin documentación.

La escala tiene un mérito adicional que no es evidente: convierte una decisión binaria en un compromiso graduado y por tanto reversible. Elevar al ancestro común puede deshacerse bajando otra vez; pasar por la URL puede deshacerse cambiando una ruta; delegar en la caché puede deshacerse pidiendo el dato de otra forma. Solo el último escalón, la promoción a global, es el que no tiene marcha atrás barata, y por eso conviene que sea el único que exija una conversación explícita con alguien que no sea quien lo propone.

// dos features necesitan el mismo pedido: no es estado compartido, es cache
function usarPedido(id: string) {
  return useQuery({ queryKey: ["pedido", id], queryFn: () => traerPedido(id) })
}
// la deduplicacion la hace la cache; ninguna de las dos features posee el dato
// y el ciclo de vida lo dicta la frescura, no el arbol de componentes

Merece la pena detenerse en la duplicación, que la ortodoxia trata como el mal absoluto y que en materia de estado tiene una versión perfectamente legítima. Duplicar la representación de un dato es peligroso; duplicar un valor que ya no volverá a cambiar no lo es. Cuando el flujo de compra copia el precio en el momento de confirmar el pedido, no está duplicando el precio del catálogo: está registrando un hecho histórico que debe permanecer estable aunque el catálogo cambie mañana. La distinción entre copiar una referencia viva y capturar un valor congelado elimina, en la práctica, una fracción notable de los estados compartidos que un equipo cree necesitar.

// copia peligrosa: dos representaciones vivas del mismo hecho
estadoGlobal.precioActual = catalogo.precio(sku)   // diverge en cuanto cambie

// captura legitima: el hecho ocurrido no vuelve a cambiar
const linea = { sku, cantidad, precioConfirmado: catalogo.precio(sku) }
// no es duplicacion de estado: es un dato historico con dueno propio

Cuando ninguna alternativa encaja y la promoción resulta inevitable, la decisión debe pagar un peaje: nombrar un dueño y escribir por qué. Un estado compartido sin dueño no es un recurso común sino un bien sin propietario, y los bienes sin propietario se degradan siguiendo una dinámica conocida, en la que cada participante extrae valor añadiendo un campo y nadie asume el coste de mantener la coherencia del conjunto. El dueño no es quien más lo usa, sino quien acepta revisar los cambios y responder de la forma del dato.

💡
Comparte lecturas derivadas, no la estructura

Aunque el dato viva en un ámbito común, lo que exponen las features debe seguir siendo una lectura con nombre y no el objeto en bruto. Compartir sesion.permisos obliga a que cualquier cambio en el modelo de permisos toque a todos los consumidores; compartir puedeEditarFactura deja que el modelo evolucione detrás de un nombre estable. La regla se resume en que lo global sea el hecho, no su representación.

Cuando lo común deja de tener dueño

Hay síntomas fiables de que un ámbito compartido ha cruzado el punto de no retorno, y todos son observables sin necesidad de auditar el código entero. El primero es que el tipo del estado global crece de forma monótona durante trimestres: nunca se elimina un campo, solo se añaden. El segundo es que su modificación aparece en los cambios de todos los equipos, lo que significa que se ha convertido en un punto de coordinación obligatoria y por tanto en un cuello de botella organizativo. El tercero es que nadie sabe responder qué pasa si un campo concreto se queda obsoleto.

Los tres síntomas comparten una causa común que conviene enunciar sin eufemismos: el ámbito compartido dejó de tener una respuesta a la pregunta de para qué sirve. Un contenedor con propósito puede rechazar peticiones, porque hay campos que evidentemente no encajan en él. Un contenedor definido por su alcance y no por su propósito no puede rechazar nada, ya que cualquier dato que dos features necesiten cumple por definición el único criterio existente. La degradación, por tanto, no empieza cuando se añade el primer campo dudoso, sino antes, cuando el ámbito se nombró por dónde llega en vez de por qué contiene.

// sintoma: el tipo compartido acumula campos de todos los equipos
type EstadoGlobal = {
  usuario: Usuario
  tema: Tema
  ultimoPedidoVisto: string | null      // anadido por checkout hace un ano
  bannerCerrado: boolean                // anadido por growth
  borradorComentario: string            // anadido por soporte, ya sin uso
}

// remedio: ambitos con nombre y dueno, no un objeto unico
type Sesion = { usuario: Usuario; permisos: ReadonlySet<Permiso> }
type Apariencia = { tema: Tema; densidad: Densidad }

Hay un cuarto síntoma más sutil y más grave que los tres anteriores, porque no se manifiesta en el código sino en la conversación: las discusiones de diseño empiezan a girar en torno a qué hará el estado global en vez de qué hará el producto. Cuando alguien propone una funcionalidad y la primera objeción es que el contenedor común no soporta esa forma, el ámbito compartido ha dejado de ser un medio para convertirse en la restricción principal del diseño, y a partir de ahí el producto se moldea según lo que la estructura de datos admite. Es el punto en el que la deuda deja de costar tiempo y empieza a costar posibilidades.

El inventario que hace posible cualquier cura se construye en una tarde y se mantiene automáticamente después. Consiste en una tabla con una fila por campo compartido y cuatro columnas: quién escribe, quién lee, quién responde de la forma y desde cuándo no cambia. La tercera columna es la que produce el efecto inmediato, porque obliga a escribir un nombre propio junto a cada campo y revela, sin necesidad de discutir nada, cuántos de ellos no tienen ninguno.

// inventario minimo del ambito compartido, versionado junto al codigo
// campo               escribe        lee                  dueno    ultimo cambio
// usuario             auth           todas                auth     2026-05
// tema                ajustes        todas                diseno   2025-12
// ultimoPedidoVisto   checkout       pedidos              nadie    2024-08
// bannerCerrado       growth         nadie desde 2025-11  nadie    2025-03

La cura no es partir el objeto en trozos más pequeños del mismo objeto, sino sustituir un contenedor sin dueño por varios ámbitos con nombre, cada uno con su responsable y su superficie mínima. La diferencia es de gobierno y no de tamaño: tres objetos anónimos se degradan igual que uno. Lo que detiene la degradación es que exista alguien a quien preguntar antes de añadir un campo, porque esa fricción, mínima pero real, filtra exactamente las promociones oportunistas que la comodidad convierte en deuda.

Existe además un camino de salida que no requiere una gran refactorización y que funciona porque aprovecha la única ventana en la que retirar es barato. Cada vez que alguien vaya a añadir un campo al ámbito común, la regla es que primero debe intentar retirar uno, y para elegir cuál dispone del inventario de lecturas que la revisión periódica mantiene. No es una penalización arbitraria: es el mecanismo más simple conocido para que un recurso común no crezca de forma monótona, y su efecto acumulado en un año es mayor que el de cualquier campaña puntual de limpieza, porque actúa exactamente en el momento en que el contexto está fresco y alguien tiene motivo para pensar en el asunto.

El estado compartido es una decisión organizativa disfrazada de decisión técnica

Cuando un equipo discute si un dato debe ser global, cree estar resolviendo un problema de arquitectura y en realidad está fijando quién tendrá que hablar con quién durante los próximos años. Cada porción de estado compartido establece un punto de sincronización entre las personas que pueden modificarla, y ese punto no aparece en ningún diagrama pero determina el ritmo real del desarrollo: dos equipos que comparten un tipo comparten un calendario, porque cualquier cambio en la forma del dato obliga a coordinar despliegues, revisiones y migraciones. La ley de Conway se suele citar en su versión descriptiva, la de que el sistema imita la estructura de comunicación de la organización, pero la versión útil es la inversa: la estructura de estado compartido que se elige hoy fabrica la estructura de comunicación que se sufrirá mañana. De ahí que promover un dato al ámbito común no sea una micro decisión reversible, sino la creación de una dependencia organizativa permanente a cambio de ahorrar unas líneas de paso de parámetros. Y de ahí también que el criterio de promoción no pueda ser técnico. La pregunta correcta no es si dos features necesitan el mismo dato, porque eso ocurre continuamente y casi siempre por accidente, sino si es aceptable que sus equipos queden atados por él. Cuando la respuesta se formula así, la mayoría de las promociones que parecían obvias dejan de serlo, y las pocas que sobreviven, la identidad, las preferencias, las capacidades del entorno, resultan ser precisamente aquellas en las que la coordinación ya era inevitable porque el dominio la imponía antes que el código.

⚔️ Audita lo que llamas compartido
  1. Lista todos los campos de tu ámbito global y anota, para cada uno, qué features lo leen y cuáles lo escriben.
  2. Aplica las tres marcas de lo transversal a cada campo y clasifícalos en genuinos, dudosos y falsos.
  3. Elige el falso transversal más usado y sustitúyelo por una transferencia explícita a través de la ruta o del parámetro de la operación.
  4. Busca los campos que en realidad son estado de servidor y trasládalos a la caché de consultas, midiendo cuántas líneas de sincronización desaparecen.
  5. Asigna dueño y superficie mínima a los que queden, y escribe en el repositorio la justificación de cada uno de ellos.