wandres.dev
DETECTAR CONFLICTOS · y por qué LWW pierde datos

Las alternativas: conservar, trocear, fusionar por significado

Hay tres familias de estrategias más allá de gana-el-último y ninguna es gratis: conservar todos los valores, bajar la granularidad y fusionar según el tipo de dato pagan con metadatos, con expresividad o con atención del usuario.

⏱ 18 min

Establecido que gana-el-último converge y aun así destruye, la pregunta pasa a ser qué se pone en su lugar. La respuesta no es una técnica sino tres familias, y lo primero que conviene entender es que ninguna de ellas elimina el coste: lo cambia de moneda. La primera familia conserva todos los valores concurrentes y paga con complejidad en el lector y con la atención de alguien que tendrá que decidir. La segunda trocea el dato hasta que las escrituras dejan de solaparse y paga con metadatos y con la posibilidad de fabricar estados que nadie escribió. La tercera define la fusión a partir del significado del tipo de dato y paga con expresividad, porque obliga a modelar el dominio con las estructuras que saben fusionarse. Elegir bien no consiste en encontrar la familia superior, que no existe, sino en saber qué moneda puedes permitirte para cada dato concreto.

🎯 Al terminar esta lección sabrás
  • Implementar el registro multivalor y entender por qué cambia el tipo que ven todos los lectores.
  • Usar la granularidad como palanca deliberada, conociendo su límite en las invariantes cruzadas.
  • Reconocer la fusión semántica por tipo de dato y las decisiones de política que esconde.
  • Comparar las tres familias por coste de metadatos, de expresividad y de atención humana.

Conservar todos los valores: el registro multivalor

La alternativa más honesta a elegir un ganador es no elegirlo. Un registro multivalor sustituye la pregunta qué valor queda por qué valores siguen vivos, y su fusión devuelve el conjunto de escrituras que nadie ha dominado causalmente: si dos son concurrentes, sobreviven las dos; si una domina a la otra, la dominada desaparece porque su autor la vio y decidió reemplazarla. La estructura converge sin dificultad, porque unir conjuntos es conmutativo, asociativo e idempotente, y su contenido es siempre la anticadena de valores actualmente incomparables.

// Sobreviven exactamente los valores que nadie domina causalmente
function fusionarMultivalor(izq, der) {
  const candidatos = [...izq, ...der];
  return candidatos.filter(
    (v) => !candidatos.some((otro) => comparar(otro.vv, v.vv) === "otro sucede despues")
  );
}

// Resolver es escribir un valor que domina a todos los hermanos
function resolverCon(valor, hermanos, replica) {
  const vv = unirVectores(hermanos.map((h) => h.vv));
  return [{ valor, vv: incrementar(vv, replica), replica }];
}

Lo decisivo de esta familia no es su implementación, que es breve, sino que cambia el tipo del dato en toda la aplicación. El campo que era una cadena pasa a ser una lista de cadenas, y ese cambio se propaga a cada consulta, cada plantilla y cada validación. Es incómodo, y es exactamente por eso que resulta valioso: el sistema de tipos deja de permitirte ignorar la existencia del conflicto. Donde gana-el-último ocultaba la ambigüedad tras una interfaz que mentía, el registro multivalor obliga a que cada punto de lectura declare qué hace cuando hay más de un valor.

El coste que hay que administrar de verdad es la proliferación de hermanos. Cada resolución debe escribirse como un valor nuevo cuyo contexto causal domina a todos los anteriores; si el cliente resuelve mostrando uno y guardando sin ese contexto, los hermanos no se colapsan y se acumulan sincronización tras sincronización hasta que un registro guarda decenas de versiones. Los sistemas que popularizaron esta técnica —la familia de almacenes inspirados en Dynamo— documentaron el fenómeno con precisión y también su remedio: la resolución no es una lectura, es una escritura que reconoce lo que vio.

💡
La resolución diferida es una opción legítima y muy infravalorada

Conservar ambos valores no obliga a preguntar de inmediato. Puedes almacenar los hermanos, mostrar el más plausible según un criterio cualquiera y marcar discretamente que hay otra versión disponible, dejando que la resolución ocurra cuando alguien tenga contexto para decidir. Esa demora convierte una interrupción en una nota al margen y no pierde absolutamente nada, porque el valor descartado sigue existiendo. Es la diferencia entre un sistema que exige atención inmediata y uno que solo pide que la información esté disponible cuando haga falta, y en la mayoría de los productos la segunda postura es la correcta.

Bajar la granularidad: fusión campo a campo

La segunda familia no cambia la política sino la unidad sobre la que se aplica. Si dos escrituras concurrentes tocan campos distintos del mismo documento, tratarlas como un conflicto es un artefacto de haber elegido el documento como unidad; con el campo como unidad, sencillamente no se solapan y ambas sobreviven sin que nadie decida nada. La técnica es barata de implantar sobre un sistema existente y su rendimiento es inmediato: en aplicaciones de formularios y fichas, donde las ediciones concurrentes rara vez coinciden en el mismo campo, la tasa de conflictos cae de forma drástica.

// Cada campo lleva su propia historia y se fusiona por separado
function fusionarDocumento(a, b, politicas) {
  const campos = new Set([...Object.keys(a), ...Object.keys(b)]);
  const salida = {};
  for (const c of campos) {
    if (!(c in a)) { salida[c] = b[c]; continue; }
    if (!(c in b)) { salida[c] = a[c]; continue; }
    salida[c] = politicas[c] ? politicas[c](a[c], b[c]) : fusionarMultivalor([a[c]], [b[c]]);
  }
  return salida;
}

Sus dos costes ya asomaron en la primera lección y ahora conviene cuantificarlos. El primero es de contabilidad: cada campo necesita su propia información causal, de modo que los metadatos dejan de crecer con el número de documentos y pasan a crecer con el número de documentos por el número de campos. En modelos anchos, esa multiplicación puede superar al propio contenido. El segundo es el conflicto semántico: campos ligados por una invariante fusionados por separado producen combinaciones que ninguna réplica generó, y el sistema no puede detectarlas porque desde su punto de vista no hubo solapamiento en ningún sitio.

La regla de diseño que resuelve el segundo coste es sencilla de enunciar y exige conocer el dominio: la unidad de fusión debe ser cerrada bajo las invariantes. Los campos que se restringen entre sí forman un bloque que se fusiona a la vez o se valida después de fusionar; los campos genuinamente independientes pueden separarse sin riesgo. Bajar la granularidad por debajo del bloque invariante no es afinar, es delegar en el algoritmo de fusión una contradicción que el algoritmo no sabe que existe.

Fusión semántica: que el tipo de dato haga el trabajo

La tercera familia es la más ambiciosa y la que da nombre a la mitad del track. En vez de detectar el conflicto y aplicarle una política, se elige para cada dato una estructura cuya operación de fusión ya expresa lo que significa el dato, de manera que el caso concurrente tiene un resultado determinado por construcción y no queda nada que decidir. Un contador no compara valores finales sino que suma las contribuciones de cada réplica. Un conjunto no elige entre dos versiones sino que combina altas y bajas con una regla explícita. Un texto no reemplaza cadenas sino que ordena inserciones identificadas de forma estable.

// Conjunto con prioridad al alta: cada elemento se anade con una etiqueta unica
function anadir(conj, elemento, replica) {
  const etiqueta = `${replica}:${crypto.randomUUID()}`;
  return { ...conj, [elemento]: new Set([...(conj[elemento] ?? []), etiqueta]) };
}

function quitar(conj, elemento) {
  const copia = { ...conj };
  delete copia[elemento]; // solo elimina las etiquetas que esta replica ha visto
  return copia;
}
// Un alta concurrente con una baja sobrevive: su etiqueta era desconocida para quien borro

Conviene resistir la impresión de que aquí los conflictos han desaparecido. Lo que ha ocurrido es que la decisión se ha adelantado del tiempo de ejecución al tiempo de diseño. El fragmento anterior da prioridad al alta: un elemento añadido concurrentemente con su borrado permanece. Existe la variante simétrica que da prioridad a la baja, y ambas convergen igual de bien. Cuál es la correcta no lo dice la matemática, lo dice el dominio: para una lista de la compra compartida, probablemente que sobreviva el alta; para una lista de permisos de acceso, casi con seguridad que sobreviva la baja. Sigue siendo una política; lo que cambia es que se elige una vez, por escrito y con conocimiento de causa, en lugar de repetirse implícitamente en cada escritura.

🗃️

Multivalor: paga en lectura

Metadatos moderados y ninguna pérdida, a cambio de cambiar el tipo en toda la aplicación y de necesitar un punto donde alguien decida.

🧩

Granularidad: paga en contabilidad

Implantación barata sobre lo existente y caída fuerte de la tasa de conflictos, a cambio de metadatos multiplicados y de conflictos semánticos.

🧬

Semántica: paga en expresividad

Cero decisiones en ejecución y convergencia demostrable, a cambio de modelar el dominio con las estructuras disponibles y de historia que crece.

🙋

Humano: paga en atención

La única familia que puede acertar con la intención real, y la más cara de todas porque el recurso que consume no es reponible.

Merece la pena señalar el cambio de mentalidad que esta familia exige y que suele ser el verdadero obstáculo para adoptarla. Deja de modelarse el estado y pasa a modelarse la operación: en vez de guardar que la lista contiene tres elementos, se guarda que alguien añadió uno, alguien añadió otro y alguien quitó un tercero, y el estado se obtiene aplicando todo lo conocido. Esa inversión es la misma que separa a una tabla de saldos de un libro de asientos contables, y trae consigo las mismas ventajas —auditabilidad completa, reconstrucción desde cero, capacidad de explicar cómo se llegó a un valor— y el mismo inconveniente, que es que la historia pesa y hay que decidir qué hacer con ella.

El coste específico de esta familia es doble. El primero es de expresividad: no todo dominio se expresa cómodamente con estructuras que conmutan, y forzar el modelo para que encaje puede producir esquemas que nadie entiende. El segundo es de crecimiento: mantener la identidad de las operaciones exige etiquetas únicas y lápidas que sobreviven a los datos que representan, de modo que la estructura acumula historia y necesita compactación, que es un problema difícil por derecho propio y al que el track dedica un bloque entero más adelante.

El coste comparado y la combinación sensata

Puestas una junto a otra, la comparación no arroja una ganadora sino un mapa. Gana-el-último cuesta un entero por dato y destruye. El multivalor cuesta un vector por dato y no destruye nada, pero traslada la decisión aguas abajo. La granularidad fina multiplica los metadatos por el número de campos y elimina los conflictos que no eran reales, dejando intactos los que sí. La fusión semántica no requiere decisión alguna en ejecución y a cambio impone la forma del modelo y acumula historia. Y la escalada al humano acierta cuando ninguna otra puede, al precio más alto que existe.

flowchart TD
D[un dato en conflicto potencial] --> Q1[es una muestra de una realidad externa]
Q1 --> LWW[gana el ultimo es adecuado aqui]
D --> Q2[existe un tipo que conmuta y expresa el dominio]
Q2 --> SEM[fusion semantica por tipo de dato]
D --> Q3[los campos son independientes entre si]
Q3 --> GRAN[bajar la granularidad al campo]
D --> Q4[el valor descartado seria una perdida real]
Q4 --> MV[conservar ambos y decidir despues]
style LWW fill:#f9e2af,color:#11111b
style SEM fill:#a6e3a1,color:#11111b
style GRAN fill:#89b4fa,color:#11111b
style MV fill:#cba6f7,color:#11111b

El árbol tiene un orden de preguntas que no es arbitrario y conviene respetarlo. Se empieza por la muestra externa porque es la única rama que autoriza descartar sin culpa, y dejarla para el final lleva a construir maquinaria para datos que no la necesitaban. Se sigue por la fusión semántica porque, cuando aplica, no deja residuo alguno. La granularidad va después porque no resuelve conflictos sino que evita solapamientos falsos. Y conservar ambos queda al final no por ser peor, sino por ser la red que recoge todo lo que las tres anteriores no supieron colocar: es el valor por defecto correcto, no el último recurso.

Lo que el diagrama sugiere y conviene subrayar es que las familias se combinan por dato y no se eligen para la aplicación entera. Un documento real mezcla campos de tipos muy distintos, y aplicarle una estrategia uniforme es garantizar que sobra maquinaria en unos y falta en otros. La misma ficha puede llevar el estado de presencia por último escritor, las etiquetas por unión de conjunto, el contador de vistas por suma de contribuciones, el cuerpo del texto por secuencia convergente y el título por conservación de ambos con aviso. Cinco políticas en un mismo registro no es incoherencia: es haber mirado cada dato.

📝
Empieza por lo que ya conmuta, que suele ser más de lo que parece

Antes de construir nada sofisticado, recorre tu modelo buscando los datos que ya tienen una fusión evidente y que estabas tratando como conflictos por inercia. Los conjuntos de etiquetas se unen, los contadores se suman, los registros que solo crecen se concatenan, los máximos se toman, los booleanos que solo pasan de falso a verdadero se disyuntan, las fechas de última visita se maximizan. En un modelo típico esa revisión resuelve una fracción sorprendente de los campos sin ninguna infraestructura nueva, y reduce el problema difícil al puñado de campos que de verdad lo eran. Es el trabajo con mejor relación entre esfuerzo y resultado de todo el nivel.

Todas las alternativas hacen lo mismo: mover la decisión a un momento en que hay más información

Vistas por separado, las tres familias parecen técnicas inconexas —un conjunto de valores, un troceado más fino, un tipo algebraico— y cuesta ver qué tienen en común más allá de no ser gana-el-último. Pero hay un eje que las ordena a todas y que, una vez visto, convierte la elección en un razonamiento en lugar de en un catálogo: cada una traslada la decisión a un instante distinto de la vida del dato, y lo único que las diferencia de verdad es cuánta información hay disponible en ese instante. Gana-el-último decide en el momento de la fusión, que es el peor instante concebible: no hay usuario presente, no hay contexto, no hay intención, solo dos valores y un número. La fusión semántica decide en el momento del diseño del esquema, meses antes de que el conflicto exista, y por eso puede acertar sin contexto: no está resolviendo un caso particular, está declarando qué significa el dato, y esa declaración es válida para todos los casos futuros a la vez. El multivalor decide en el momento de la lectura consciente, cuando hay alguien mirando la pantalla que sabe qué estaba haciendo. Y la escalada al humano decide en el instante de máxima información posible, que es también el de máximo coste. La regla de diseño que se deduce de ese eje es más nítida que cualquier tabla comparativa: empuja cada decisión hacia el momento en que exista información suficiente para tomarla, y ni un paso más allá. Empujarla demasiado atrás produce elecciones ciegas, que es exactamente el diagnóstico de la lección anterior. Empujarla demasiado adelante produce interrupciones innecesarias, que es la patología de la lección siguiente. Y el arte que separa a un sistema que la gente disfruta de uno que la gente tolera consiste en reconocer, campo por campo, cuál es ese momento. Fíjate además en que este eje explica de golpe por qué el resto del track dedica quince niveles a las estructuras convergentes y no a las interfaces de resolución: porque decidir en tiempo de diseño es, cuando es posible, estrictamente mejor que decidir en tiempo de ejecución —cuesta cero por conflicto, escala sin límite, no consume atención de nadie y se puede demostrar correcta—. La investigación en estructuras que convergen no es una elegancia académica ni un gusto por el álgebra: es el intento sistemático de mover la mayor cantidad posible de decisiones al único instante en que salen gratis. Lo que quede fuera de ese esfuerzo, y siempre quedará algo, es el territorio irreductible del diseño de producto, y a ese territorio dedicamos la última lección del nivel.

⚔️ Asigna una estrategia a cada campo
  1. Haz el inventario de campos de tu modelo y marca los que ya conmutan de forma evidente: conjuntos, contadores, máximos y registros que solo crecen.
  2. Para el resto, aplica el árbol de decisión de esta lección y anota junto a cada campo la familia elegida y la razón en una línea.
  3. Implementa el registro multivalor para al menos un campo importante y comprueba que la resolución colapsa los hermanos en lugar de acumularlos.
  4. Identifica en tu esquema los bloques cerrados bajo invariantes y verifica que ningún bloque quedó troceado entre unidades de fusión distintas.
  5. Elige un conjunto de tu dominio, decide por escrito si debe ganar el alta o la baja, y justifica la elección con un caso real de uso.
  6. Estima el coste de metadatos de tu asignación completa y compáralo con el tamaño del contenido antes de darla por buena.