Cuándo NO usar un CRDT
Tres situaciones en las que un CRDT es la herramienta equivocada: el conflicto que exige una decisión de negocio, los datos relacionales con invariantes y el problema que un servidor autoritativo resuelve con mucho menos código.
Después de cuarenta niveles construyendo estructuras que convergen, la lección más rentable del track es la que enseña a no usarlas. No por prudencia ni por equilibrio retórico, sino porque la mayor parte del coste desperdiciado en sistemas local-first no viene de elegir mal entre Yjs, Automerge y Loro: viene de haber decidido que el problema requería un CRDT cuando no lo requería. Hay tres situaciones donde esa decisión es demostrablemente equivocada, y las tres tienen la incómoda propiedad de que el CRDT funciona: converge, no falla, pasa las pruebas y se comporta exactamente como promete. El problema es que lo que promete no era lo que hacía falta, y descubrirlo tarde cuesta mucho más que descubrir que una librería era lenta.
- Reconocer el conflicto cuya resolución correcta depende de una regla de negocio y no de una estructura.
- Ver por qué las invariantes entre entidades distintas quedan fuera del contrato de convergencia.
- Estimar honestamente cuándo un servidor autoritativo resuelve el mismo problema con muchísimo menos código.
- Aplicar una prueba de tres preguntas antes de comprometerse con una librería CRDT.
El conflicto que necesita una decisión de negocio
Un CRDT resuelve conflictos adelantando la decisión al momento del diseño: eliges una semántica de fusión y a partir de ahí cada caso concurrente tiene un resultado determinado por construcción. Ese mecanismo es potentísimo mientras la decisión correcta sea la misma siempre. Deja de serlo en cuanto la resolución correcta depende de información que la estructura no tiene y que a veces ni siquiera está en el sistema.
Dos comerciales aplican descuentos distintos al mismo contrato desde dos sitios sin conexión. La fusión puede quedarse con el último, con el mayor, con el menor o conservar los dos; las cuatro convergen y las cuatro son igual de correctas desde el punto de vista algebraico. Cuál es la correcta desde el punto de vista del negocio depende de la política de aprobación, del margen, de quién tiene autoridad sobre esa cuenta y de si el cliente ya vio uno de los dos números. Nada de eso cabe en una función de fusión, y forzarlo dentro convierte una regla de negocio en un detalle enterrado en una capa que nadie revisa cuando la política cambia.
Hay una variante de este error especialmente difícil de detectar, porque parece lo contrario de un atajo. Consiste en conservar ambos valores y presentarlos al usuario para que decida, convencidos de que así se respeta la regla de negocio. Funciona cuando quien está delante de la pantalla tiene autoridad para decidir; falla en silencio cuando no la tiene, porque entonces el sistema ha convertido una decisión con responsable en una decisión que toma quien pasaba por ahí, y además ha borrado el rastro de que hubo algo que aprobar. Conservar ambos valores es una técnica buena; no es una política de negocio.
La señal de alarma es concreta y se puede buscar: si al describir la fusión de un campo necesitas usar la palabra depende, ese campo no quiere una semántica de fusión, quiere un flujo de aprobación. La diferencia práctica es enorme, porque un flujo de aprobación tiene estados visibles, responsables, registro de quién decidió y capacidad de cambiar la política sin migrar ningún dato. Una función de fusión no tiene nada de eso, y su virtud principal —decidir sin preguntar— es exactamente el defecto en este caso.
Un sistema que fusiona mal converge igual de bien que uno que fusiona bien: las réplicas quedan idénticas en los dos casos. Por eso este error no produce fallos, produce silencio. No hay excepción, no hay alerta, no hay diferencia entre dispositivos que delate nada; simplemente el descuento que un comercial aplicó dejó de existir y todo el mundo ve la misma versión coherente de una realidad equivocada. Cuando un tipo de error no se manifiesta como error, la única defensa es no cometerlo en el momento del diseño, porque ninguna prueba automatizada posterior lo va a detectar.
Datos relacionales con invariantes
La segunda situación se enunció al final de la lección anterior y merece desarrollo, porque es la más frecuente y la peor diagnosticada. La convergencia es una propiedad sobre la relación entre réplicas: garantiza que todas terminen en el mismo estado. La validez es una propiedad sobre el estado en sí: exige que ese estado cumpla ciertas reglas. Son independientes, y ninguna librería CRDT promete la segunda.
// Cada operacion es valida por separado y la fusion es invalida
// Invariante: la suma de asignaciones no puede superar el presupuesto
// Replica A, presupuesto 1000, ve 400 asignados
asignar({ partida: "obra", importe: 500 }); // total local 900, valido
// Replica B, presupuesto 1000, ve 400 asignados
asignar({ partida: "licencias", importe: 550 }); // total local 950, valido
// Tras fusionar: 400 + 500 + 550 = 1450 sobre un presupuesto de 1000.
// Ninguna replica escribio sobre lo que escribio la otra.
// No hubo conflicto que detectar: hubo una invariante global que nadie vigilaba.
Lo importante de este ejemplo es que no hay ningún punto donde una función de fusión mejor hubiera salvado la situación. El problema no está en cómo se combinan dos valores sino en que la regla habla de una suma sobre entidades distintas, y las entidades distintas no colisionan nunca. La misma forma tienen la unicidad de una clave, la integridad referencial, el cupo limitado, la exclusividad de una reserva y la mayor parte de lo que una base de datos relacional lleva décadas garantizando con transacciones y restricciones.
La forma de diagnosticar esta situación sin ambigüedad es comprobar el alcance de cada regla. Una regla que habla de un solo valor —este campo nunca está vacío, este número no es negativo— sí puede sostenerse localmente, porque cada réplica la comprueba antes de escribir y la fusión no puede romperla si la semántica está bien elegida. Una regla que habla de varios registros a la vez no puede sostenerse así, porque ninguna réplica ve el conjunto completo en el momento de escribir. Ese cambio de alcance, de uno a varios, es exactamente la frontera entre lo que un CRDT garantiza y lo que no, y es una propiedad de tu dominio que puedes clasificar en una tarde.
Existen técnicas para operar con invariantes sin coordinación en cada escritura, y todas consisten en repartir presupuesto por adelantado: cada réplica recibe una porción del cupo y solo puede gastar la suya, de modo que la suma global no se puede superar aunque nadie coordine. Funciona, y conviene saber que existe, pero el precio hay que verlo entero: introduce coordinación para redistribuir el reparto, hace que una réplica pueda agotar su porción mientras hay cupo libre en otra, y añade una capa de contabilidad que no es la parte divertida de nadie. Cuando ese precio parece razonable, adelante. Cuando no, la respuesta correcta es un punto que serialice esas operaciones.
El servidor autoritativo y la décima parte del código
La tercera situación es la más difícil de admitir porque contradice la premisa del track, y es también la que más proyectos debería detener. Hay una clase amplia de problemas donde un servidor autoritativo con escrituras optimistas en el cliente da al usuario casi toda la experiencia local-first —respuesta inmediata, tolerancia a cortes breves— con una fracción del código y sin ninguno de los compromisos estructurales de un CRDT.
flowchart TD Q[necesitas convergencia sin arbitro] --> A[la resolucion depende de una regla de negocio] A --> NA[no uses CRDT: usa un flujo de aprobacion] Q --> B[hay invariantes que ligan entidades distintas] B --> NB[no uses CRDT: serializa esas operaciones] Q --> C[un servidor autoritativo lo resuelve entero] C --> NC[no uses CRDT: escritura optimista y reintento] Q --> D[ninguna de las tres condiciones se cumple] D --> S[el CRDT es la herramienta adecuada] style NA fill:#f38ba8,color:#11111b style NB fill:#f38ba8,color:#11111b style NC fill:#f38ba8,color:#11111b style S fill:#a6e3a1,color:#11111b
La comparación honesta del coste hay que hacerla sobre el sistema completo y no sobre la línea que instala la dependencia. Adoptar un CRDT trae consigo un formato binario que hay que persistir y versionar, un modelo de datos que hay que traducir a la estructura de la librería y de vuelta, un protocolo de sincronización, una estrategia para la historia que crece, un plan de migración de esquema para dispositivos que reaparecen meses después, y un modo de depuración donde el estado que ves no es el que está escrito. Cada una de esas piezas es razonable por separado; juntas son el proyecto.
La alternativa completa cabe en muy poco código, y verlo escrito es la mejor defensa contra la sensación de que estás renunciando a algo grande.
// Escritura optimista: la parte entera de la alternativa
const pendientes = await cargarCola();
async function ejecutar(operacion) {
aplicarEnLaVista(operacion); // el usuario ve el efecto ya
pendientes.push(operacion);
await guardarCola(pendientes); // sobrevive a un cierre de pestana
drenar();
}
async function drenar() {
while (pendientes.length && navigator.onLine) {
const op = pendientes[0];
const res = await enviar(op); // el servidor arbitra y valida
if (res.rechazada) return revertirEnLaVista(op, res.motivo);
pendientes.shift();
await guardarCola(pendientes);
}
}
Las transacciones, las restricciones de unicidad y la integridad referencial siguen estando donde siempre estuvieron, y el caso de la sección anterior deja de ser un problema abierto para convertirse en un rechazo con motivo. Ese rechazo hay que saber presentarlo, y ahí está el trabajo real de esta arquitectura: qué se le dice al usuario cuando una operación que ya vio aplicada resulta inválida. Es un problema de producto acotado, con una superficie mucho menor que la de un sistema de convergencia completo.
Frente a eso, la escritura optimista contra un servidor autoritativo necesita una cola local de operaciones pendientes, un reintento con espera creciente y una regla de qué hacer cuando el servidor rechaza una operación ya reflejada en pantalla. Es sustancialmente menos código, usa la base de datos que ya sabes operar, conserva las transacciones y las restricciones que resuelven el problema de la sección anterior, y falla de formas que sabes diagnosticar. Su límite es real y hay que enunciarlo con la misma claridad: si el usuario tiene que trabajar horas sin red, si la colaboración debe funcionar sin que exista un servidor alcanzable o si la propiedad de los datos es un requisito del producto, esa arquitectura no llega y el CRDT es la respuesta correcta.
Decisión de negocio
Si al describir la fusión necesitas decir que depende, ese campo quiere un flujo de aprobación con responsables y registro, no una semántica de fusión.
Invariantes entre entidades
Cupos, unicidad, integridad referencial y sumas globales. Las entidades distintas no colisionan, así que no hay conflicto que una fusión pueda detectar.
Servidor autoritativo
Escritura optimista con cola y reintento da casi toda la experiencia con una fracción del código, mientras exista un servidor alcanzable la mayor parte del tiempo.
La prueba de las tres
Si ninguna de las tres condiciones se cumple, el CRDT es la herramienta adecuada y la comparación entre librerías vuelve a tener sentido.
La prueba de tres preguntas
| Pregunta | Si la respuesta es sí | Qué hacer en su lugar |
|---|---|---|
| ¿La resolución correcta depende de contexto ausente? | El CRDT decidirá mal en silencio | Flujo de aprobación con responsable y registro |
| ¿Hay reglas que hablen de varios registros? | La convergencia no las protege | Repartir presupuesto o serializar esas operaciones |
| ¿Trabajan sin servidor solo minutos? | La autonomía no se ejerce | Escritura optimista con cola y reintento |
Antes de comprometerse con una librería conviene pasar la decisión por tres preguntas que se responden por escrito y en este orden. Primera: ¿existe algún conflicto cuya resolución correcta dependa de contexto que el sistema no tiene? Si lo hay, ese conflicto no se resuelve con una estructura, y meterlo dentro solo consigue esconderlo. Segunda: ¿hay invariantes que liguen entidades distintas y que deban cumplirse siempre? Si las hay, decide para cada una si aceptas repartir presupuesto por adelantado o si necesitas serializar, y ninguna de las dos respuestas es la librería. Tercera: ¿cuánto tiempo real trabajan tus usuarios sin servidor alcanzable? Si la respuesta honesta se mide en minutos y no en horas, la escritura optimista probablemente basta.
Que una parte de tu sistema falle la prueba no descalifica el enfoque para el resto. Lo habitual en productos reales es que el cuerpo del documento sea territorio de CRDT sin discusión y que los permisos, las cuotas, la facturación y los identificadores únicos vivan en un servidor autoritativo con transacciones. Esa frontera no es una concesión ni una arquitectura híbrida por indecisión: es el resultado correcto de haber aplicado la prueba parte por parte en lugar de para el sistema entero. Lo único que exige es que la frontera esté escrita y que quede claro qué la cruza y en qué dirección.
La afirmación de que un CRDT permite operar sin coordinación es cierta y sistemáticamente mal entendida, y de ese malentendido salen las tres situaciones de esta lección. Lo que un CRDT hace no es eliminar la coordinación: es cambiar coordinación por renuncia. Renuncia a decidir con información que llegará después, renuncia a garantizar propiedades que hablen de varias entidades a la vez, renuncia a rechazar una operación. Esas tres renuncias son exactamente el precio, y son perfectamente razonables cuando lo que compras con ellas es trabajar horas sin red y colaborar sin que exista un árbitro. Dejan de serlo en cuanto lo que se renuncia es lo que el producto necesitaba. El primer caso de la lección renuncia a decidir con contexto, y el contexto era la regla de negocio. El segundo renuncia a las propiedades multientidad, y esas propiedades eran el cupo, la unicidad y la integridad. El tercero renuncia a la simplicidad de un árbitro que ya existía y funcionaba, a cambio de una autonomía que los usuarios no iban a ejercer. Ninguno de los tres se manifiesta como un fallo, y esa es la parte que hay que interiorizar: un CRDT mal aplicado no se rompe, converge. Converge a un estado en el que el descuento del comercial no existe, el presupuesto está sobrepasado y todo el mundo ve lo mismo con perfecta consistencia. Un sistema que falla te avisa; un sistema que converge a lo incorrecto te deja tranquilo durante meses. De ahí la formulación que conviene llevarse del nivel entero, y que es la única que hace defendible tanto usar un CRDT como no usarlo: la pregunta no es si tu sistema puede converger sin coordinación, sino si puedes permitirte lo que hay que renunciar para conseguirlo. Contestarla exige conocer el dominio, no la librería, y por eso es una decisión que no se delega y que no aparece en ninguna tabla comparativa. Cuando la respuesta es que sí puedes permitírtelo, las tres opciones del nivel vuelven a ser una elección técnica legítima y todo lo aprendido en las lecciones anteriores se aplica. Cuando la respuesta es que no, ninguna de las tres te va a salvar, y haberlo descubierto antes de escribir el código es el mejor rendimiento que este nivel puede darte.
- Recorre tu modelo campo a campo y marca aquellos cuya fusión correcta no puedes describir sin usar la palabra depende.
- Enumera todas las invariantes que ligan entidades distintas y clasifica cada una en repartir presupuesto por adelantado o serializar.
- Mide o estima con datos reales cuánto tiempo seguido trabajan tus usuarios sin servidor alcanzable, en minutos u horas.
- Escribe el presupuesto completo de adoptar un CRDT: formato, traducción del modelo, protocolo, historia, migraciones y depuración.
- Escribe el presupuesto de la alternativa con escritura optimista y reintento, y compáralos sobre el sistema entero y no sobre la dependencia.
- Dibuja la frontera entre la parte que será CRDT y la que será autoritativa, y documenta qué la cruza y en qué dirección.