wandres.dev
QUÉ ES EL ESTADO · la raíz de la complejidad

Estado esencial vs accidental

El ensayo Out of the Tar Pit (2006), de Ben Moseley y Peter Marks, es la lente más lúcida para entender por qué el software se convierte en un pantano: la complejidad tiene dos naturalezas, la esencial —inherente al problema— y la accidental —la que nos infligimos al resolverlo—. Aquí se aplica esa distinción al estado, se demuestra que la mayor fuente de complejidad accidental es el estado que no debería existir, y se aprende a clasificar cada dato como esencial, derivado o superfluo.

⏱ 15 min

En 2006, Ben Moseley y Peter Marks publicaron Out of the Tar Pit, quizá el ensayo más lúcido jamás escrito sobre por qué el software se vuelve un pantano. Su tesis es simple y demoledora: la complejidad tiene dos naturalezas —la esencial, inherente al problema, y la accidental, la que añadimos al resolverlo— y la mayor fuente de complejidad accidental es el estado. Distinguir una de otra es la habilidad que separa a quien apaga incendios de quien no los provoca.

🎯 Al terminar esta lección sabrás
  • Distinguir la complejidad esencial —inherente al problema— de la accidental —autoinfligida—.
  • Entender la tesis central de Out of the Tar Pit: el estado es la mayor fuente de complejidad accidental.
  • Clasificar cada dato de un sistema como estado esencial, derivado o superfluo.
  • Adoptar el hábito de eliminar estado accidental antes de intentar gestionarlo mejor.

Dos complejidades: esencia y accidente

Fred Brooks, en No Silver Bullet (1986), partió la dificultad del software en dos. La complejidad esencial es la que está en el problema mismo: si un banco debe llevar saldos, llevar saldos es difícil por naturaleza y ninguna herramienta lo evitará. La complejidad accidental es la que introducimos al construir la solución: el lenguaje, las capas, las copias, los apaños. Brooks predijo que ninguna “bala de plata” tumbaría la primera; el margen de mejora está casi todo en la segunda.

Moseley y Marks afilaron el hacha y señalaron al culpable. Identificaron tres fuentes de complejidad —el estado, el control (el orden en que pasan las cosas) y el volumen de código— y sentenciaron que, de las tres, el estado es la más corrosiva, porque hace que el comportamiento dependa de la historia. Un sistema sin estado se entiende leyéndolo; uno con estado exige además saber cómo llegó a donde está.

La palabra “accidental” no es un insulto: no significa hecho a la ligera, sino no impuesto por el problema. Un lenguaje, un framework o una capa de caché pueden ser decisiones razonables y aun así ser complejidad accidental, porque el problema no las pedía. La meta no es alcanzar cero complejidad accidental —imposible—, sino reconocerla como tal para no confundirla con lo esencial y poder recortarla en cuanto estorbe.

La distinción se vuelve operativa con un ejemplo. Que un banco recuerde el saldo de cada cuenta es estado esencial: el problema lo exige. Pero una copia del saldo cacheada en la interfaz, un booleano saldoSucio para saber si hay que refrescar, o un total recalculado y guardado “por rendimiento” son estado accidental: no los pide el problema, los inventamos nosotros, y cada uno es una fuente nueva de discrepancias.

En un sistema real la proporción sorprende: si haces el inventario honesto, el estado esencial suele ser una fracción pequeña, y casi todo lo demás es derivado o espejo que se coló sin permiso. Estos ejemplos cotidianos afinan el ojo:

Dato Naturaleza Por qué
El saldo de una cuenta Esencial El dominio exige recordarlo; nada lo reemplaza
El total del carrito Derivado Es la suma de los ítems; calcúlalo, no lo guardes
isLoading más isError Accidental Una sola fase disfrazada de flags independientes
Copia del perfil del servidor Espejo Su verdad vive en el servidor; es una cache
El nombre completo Derivado Es nombre más apellido; puro cálculo
🎯

Esencial

Lo exige el dominio: el saldo, el pedido, la identidad del usuario. Irreducible; ninguna arquitectura lo elimina. Es el estado que de verdad hay que gobernar.

🧮

Derivado

Se calcula de lo esencial: el total, el nombre completo, la lista filtrada. Guardarlo es accidental —creas una copia que hay que mantener sincronizada—.

🪞

Espejo

Una copia de datos cuya verdad vive en otro sitio, casi siempre el servidor. Mantenerla a mano es accidental: para eso existen las caches, no tu store.

⚗️

La prueba

¿Puedo borrar este dato sin cambiar lo que el software hace para el usuario? Si puedo recomputarlo o volver a pedirlo, era accidental. Casi todo lo es.

El estado accidental que todos escribimos

El caso de manual es el trío isLoading, isError, data: tres campos para representar una sola realidad esencial, la fase de una petición. El problema es que tres flags independientes describen ocho combinaciones, y la mayoría son imposibles. isLoading y isError a la vez no significa nada, pero el tipo lo permite, y tarde o temprano alguien lo produce.

// Accidental: tres flags para una sola realidad. Permite estados imposibles.
type Peticion = { isLoading: boolean; isError: boolean; data: Usuario | null };
// isLoading=true e isError=true a la vez: representable, pero sin sentido.

// Esencial: una union discriminada. Solo existen los casos que el problema tiene.
type Peticion2 =
  | { fase: "inicial" }
  | { fase: "cargando" }
  | { fase: "error"; mensaje: string }
  | { fase: "exito"; data: Usuario };

La unión discriminada no gestiona mejor el estado accidental: lo elimina. Pasa de ocho combinaciones a cuatro, y las cuatro son legítimas. Los estados imposibles dejan de ser representables, y con ellos desaparece una familia entera de bugs sin escribir una sola comprobación. Este es el patrón que verás formalizado en las máquinas de estado del nivel 9; aquí basta con reconocer el olor: varios flags que en realidad describen un único “en qué punto estamos”.

La otra forma común de estado accidental es guardar lo derivable: un nombreCompleto almacenado junto a nombre y apellido, una listaFiltrada guardada junto a la lista y el filtro, un total persistido junto a los ítems. Cada copia crea un invariante —“esto debe coincidir con aquello”— que nada salvo tu disciplina sostiene, y la disciplina falla. Derivar en vez de guardar convierte ese invariante frágil en una verdad por construcción.

El arreglo es casi siempre el mismo gesto: borrar el campo duplicado y sustituirlo por una función que lo calcula desde la única fuente que sí es esencial.

// Accidental: el total guardado junto a los items. Dos verdades que sincronizar.
const carrito = { items: [{ precio: 10 }, { precio: 25 }], total: 35 };
carrito.items.push({ precio: 5 }); // total ahora miente: dice 35, deberia ser 40

// Esencial mas derivado: los items son la fuente; el total se calcula.
const total = (items: { precio: number }[]) =>
  items.reduce((suma, it) => suma + it.precio, 0);
total(carrito.items); // siempre coherente, nada que mantener a mano

Fíjate en que el estado esencial —los ítems— es irreducible, pero el total deja de ser estado y se vuelve una vista. Esa es, en miniatura, toda la estrategia del nivel: encoger el núcleo que de verdad hay que recordar y expandir la zona que solo se calcula.

La tesis: minimizar antes que gestionar

La propuesta radical del ensayo es la Functional Relational Programming: guardar el estado esencial en relaciones, expresar la lógica esencial como funciones puras sobre esas relaciones, y empujar todo lo accidental —estado mutable, orden, efectos— a una capa fina y controlada en los márgenes. No hace falta adoptar el modelo al pie de la letra para llevarse la lección transferible: todo estado debe justificarse como esencial; si se puede derivar, es accidental y debe calcularse, no almacenarse.

De aquí sale un cambio de mentalidad que define la seniority. El principiante pregunta “¿qué librería de estado uso?”; el experimentado pregunta “¿cuánto de este estado debería existir siquiera?”. Ninguna librería te salva del estado accidental: Redux, Zustand o los signals organizan el pantano, pero no lo secan. El trabajo de secarlo —recortar copias, colapsar flags, derivar en vez de duplicar— es previo a cualquier herramienta, y ninguna lo hace por ti.

Hay una consecuencia estratégica en esto. Como el estado accidental se multiplica con el tamaño del sistema, atacarlo temprano rinde intereses compuestos: cada copia que no creas te ahorra escribir su sincronización, sus pruebas y sus bugs futuros. El coste de eliminar estado accidental crece con el tiempo —cuanto más código depende de él, más caro es quitarlo—, así que la ventana barata para hacerlo es siempre ahora.

El pantano no es falta de herramientas, es exceso de estado accidental

Out of the Tar Pit debe su nombre a una imagen: el software heredado como brea donde cada paso cuesta más que el anterior. Es tentador creer que se sale del pantano con mejores herramientas —un framework más moderno, una librería de estado más lista—, pero el ensayo demuestra lo contrario: se sale teniendo menos estado, no gestionándolo mejor. Cada dato accidental que eliminas no es una micro-optimización; es una rama entera del árbol de combinaciones que se poda, una clase de bug que deja de poder ocurrir, una sincronización que ya no hay que vigilar. Por eso el programador experto no se distingue por gestionar el estado con más destreza, sino por tener mucho menos estado que gestionar. Mira el estado de cualquier sistema atascado y contarás copias que espejean, flags que se contradicen, cachés que nadie invalida bien: complejidad que nadie pidió y que se paga en cada cambio futuro. La disciplina no es elegir la herramienta de moda; es preguntar de cada dato, sin piedad, si tiene derecho a existir. La mayoría no lo tiene, y descubrirlo es el trabajo real.

Un método para clasificar

La teoría se vuelve práctica con un procedimiento. Ante cada dato del sistema, hazle tres preguntas en orden. ¿Lo exige el problema en sí, aunque cambiara toda la implementación? Entonces es esencial: consérvalo. ¿Se puede calcular a partir de otro estado que ya tienes? Entonces es derivado: no lo guardes, deriva. ¿Es una copia de algo cuya verdad vive fuera —el servidor, la URL, el disco—? Entonces es un espejo: trátalo explícitamente como una cache, con su política de invalidación.

flowchart TD
D[un dato del sistema] --> Q1{lo exige el problema}
Q1 -->|si| E[esencial: mantenlo]
Q1 -->|no| Q2{se calcula de otro estado}
Q2 -->|si| A1[accidental: derivarlo]
Q2 -->|no| Q3{es copia de una verdad externa}
Q3 -->|si| A2[accidental: es una cache]
Q3 -->|no| A3[sospechoso: probablemente sobra]
style E fill:#a6e3a1,color:#11111b
style A1 fill:#fab387,color:#11111b
style A2 fill:#fab387,color:#11111b
style A3 fill:#f38ba8,color:#11111b

Lo que sobreviva a las tres preguntas es tu estado esencial, y suele ser mucho menos de lo que temías. El resto no desaparece del programa, pero cambia de naturaleza: deja de ser algo que mutas y sincronizas y pasa a ser algo que calculas o revalidas. Esa reclasificación, hecha con honestidad, encoge el espacio de estados de la lección anterior y prepara el terreno para las tres metas de la siguiente.

⚠️
El espejismo del rendimiento

Casi todo el estado accidental se cuela con la excusa del rendimiento: “guardo el total para no recalcularlo”. Pero derivar es barato, y la memoización (nivel 6) lo hace casi gratis cuando de verdad importa; en cambio, un dato duplicado se paga en cada cambio y en cada bug de sincronización, para siempre. Optimiza cuando midas que hace falta, no por miedo. La copia prematura ha creado más pantanos que la lentitud jamás.

📝
La pregunta que precede a toda herramienta

Antes de abrir la documentación de cualquier librería de estado, haz el inventario: lista cada dato del módulo y etiquétalo como esencial, derivado o espejo. La forma del problema cambia por completo según el resultado. Si casi todo es derivado, no necesitas un store, sino buenas funciones de cálculo. Si casi todo es espejo, no necesitas Redux, sino una cache como TanStack Query. La herramienta correcta cae por su propio peso una vez que sabes qué naturaleza tiene tu estado, y nunca antes.

⚔️ Clasifica sin piedad
  1. Toma un objeto de estado de tu app y marca cada campo como esencial, derivado o espejo, usando el árbol de decisión.
  2. Encuentra un trío de flags booleanos (isLoading/isError/…) y reescríbelo como unión discriminada. Cuenta cuántos estados imposibles eliminaste.
  3. Busca un valor que guardes pero podrías derivar. Bórralo del estado y calcúlalo al vuelo. ¿Se rompió algo, o solo desapareció una sincronización?
  4. Estima a ojo qué proporción del estado de un módulo tuyo es esencial. Si supera el 40 por ciento, probablemente aún no miraste con suficiente crueldad.