Cuando la autoridad central es el producto
Banca, reservas, inventario y subastas comparten un rasgo: la invariante global es el producto y no un detalle de implementación, y ninguna réplica local puede decidirla sola.
Un curso que solo vende su tema es propaganda, y este nivel entero existe para pagar esa deuda. Las lecciones anteriores han construido un argumento honesto a favor de mover el dato al cliente; esta empieza a construir el argumento contrario, y lo hace por el caso más limpio y menos discutible de todos: aquel en el que la decisión centralizada no es una limitación heredada de la arquitectura cliente-servidor, sino literalmente el servicio que se vende. Un banco que no puede afirmar cuánto dinero hay en una cuenta no es un banco lento, es otra cosa. Un sistema de reservas que a veces vende dos veces el mismo asiento no tiene un fallo, tiene otro modelo de negocio. Cuando la invariante global es el producto, replicarla es venderla rota, y hay una parte de esta afirmación que ni siquiera es una opinión de ingeniería: es un teorema.
- Distinguir las invariantes locales por objeto de las invariantes globales que cruzan réplicas.
- Reconocer los cuatro dominios canónicos donde la autoridad central no se negocia.
- Entender por qué la convergencia de un CRDT no implica corrección del dominio.
- Usar el coste de una violación como criterio para decidir si el optimismo es admisible.
La invariante global no es un detalle de implementación
Una invariante es un predicado que debe cumplirse sobre el estado completo del sistema en todo instante. La distinción que decide esta lección no es entre invariantes fáciles y difíciles, sino entre las que se pueden comprobar mirando un solo objeto y las que exigen mirar todos a la vez. Que el título de un documento no esté vacío se comprueba con el documento delante. Que la suma de los reintegros no exceda el saldo, que no haya dos reservas para el mismo asiento o que exista exactamente un ganador son predicados que cuantifican sobre el conjunto entero, y ninguna réplica ve el conjunto entero por definición: una réplica es, precisamente, una vista parcial que puede escribir sin preguntar.
Esa intuición tiene un nombre formal y un resultado detrás. La confluencia de invariantes dice que una invariante global se puede mantener sin coordinación si y solo si la fusión de dos estados válidos produce siempre un estado válido. saldo >= 0 no cumple esa condición: dos réplicas con saldo cien, cada una retirando ochenta, sostienen estados individualmente válidos cuya fusión es inválida. La unicidad tampoco la cumple, ni el aforo máximo, ni la asignación exclusiva de un identificador. El teorema CALM llega por el otro lado a la misma frontera: un programa admite implementación distribuida sin coordinación si y solo si es monótono, y toda regla de la forma no existe ningún otro es no monótona por construcción.
flowchart TB A[invariante del dominio] --> B[se comprueba con un solo objeto delante] A --> C[cuantifica sobre todas las replicas] B --> D[replicable: el cliente puede decidir] C --> E[la fusion de dos estados validos es siempre valida] E --> F[si: replicable con cuidado] E --> G[no: exige un punto de serializacion] G --> H[hay servidor, se llame como se llame] style D fill:#a6e3a1,color:#11111b style F fill:#a6e3a1,color:#11111b style H fill:#f38ba8,color:#11111b
Conviene subrayar por qué esto no es pesimismo sino economía. La coordinación no está prohibida en un sistema local-first; lo que está prohibido es fingir que no hace falta. Un diseño maduro localiza el punto exacto donde la coordinación es imprescindible, lo hace pequeño y lo aísla, en lugar de esparcirlo por toda la aplicación en forma de peticiones síncronas. La literatura distingue con precisión dos posturas que en la práctica se confunden: libre de coordinación, que significa que jamás se necesita un acuerdo, y evitando la coordinación, que significa que se paga solo cuando la invariante corre peligro real. La segunda es la postura profesional y no se puede adoptar sin haber inventariado antes las invariantes.
Ese inventario también corrige el uso perezoso del teorema CAP que circula por las presentaciones. CAP no dice elige dos de tres: dice que durante una partición hay que elegir entre responder y ser consistente, y no dice nada sobre el resto del tiempo. La consecuencia es que la pregunta correcta nunca es si tu sistema es AP o CP en abstracto, sino qué operación concreta prefiere fallar durante una partición y cuál prefiere aceptar. En un producto real esa respuesta cambia por endpoint: leer el histórico puede seguir disponible mientras firmar una transferencia se niega, y esa granularidad es donde vive todo el diseño interesante.
Antes de discutir motores de sincronización, formula la invariante en una frase y busca en ella las palabras ninguno, único, exactamente, como máximo o el total. Si aparecen y el ámbito es global, el dato necesita un punto donde una escritura pueda ser rechazada. Un almacén replicado nunca rechaza: acepta y funde. Aceptar y funde es exactamente lo contrario de lo que hace cumplir una invariante.
El catálogo de los dominios que no negocian
Los cuatro dominios que siguen no son una lista arbitraria: son las cuatro formas canónicas que adopta una invariante no confluente en un producto real. Casi cualquier caso difícil que encuentres es una variante de alguno de ellos, y reconocer la forma ahorra semanas de discusión sobre herramientas.
Banca y libro mayor
La invariante es aritmética y auditada: partida doble, saldo no negativo, trazabilidad regulatoria. El banco no almacena dinero, almacena la afirmación autorizada de quién lo tiene. Replicar esa afirmación es replicar la autoridad.
Reservas de recurso finito
Asientos, habitaciones, franjas de cita. La unicidad sobre un recurso indivisible no admite fusión: dos reservas del asiento catorce no se combinan en una reserva media. O se rechaza una, o el producto miente.
Inventario y última unidad
Con existencias abundantes el error es barato y se puede compensar. Con la última unidad, la venta doble es un incumplimiento. La invariante no es constante: se endurece justo cuando el recurso escasea, que es cuando más tráfico hay.
Subastas y ordenación
Aquí el orden temporal no es un medio para lograr el resultado: es el resultado. Y los participantes tienen incentivos adversarios, así que cualquier marca de tiempo generada en el cliente es una superficie de ataque, no un dato.
| Dominio | Invariante que lo define | Coste de una violación | Escape parcial |
|---|---|---|---|
| Banca | El saldo nunca queda por debajo de cero | Legal y regulatorio, sin techo | Ninguno en el libro mayor |
| Reservas | Un recurso indivisible, un titular | Compensación y reputación | Sobreventa deliberada y tarifada |
| Inventario | Las unidades vendidas no superan las existentes | Monetario, acotado si hay stock | Reparto de presupuesto por región |
| Subastas | El orden de llegada determina el ganador | Manipulación del mercado | Ninguno, el orden es el producto |
La columna del escape es la más instructiva de la tabla porque muestra que estos dominios no son igual de rígidos. El inventario admite una degradación elegante mientras haya holgura, y las reservas admiten una violación tarifada porque existe un mecanismo social de compensación. La banca y las subastas no admiten nada, y por motivos distintos: una porque el regulador no acepta aproximaciones, la otra porque el resultado del producto es exactamente la propiedad que se estaría relajando.
El cuarto merece un párrafo aparte porque desmonta un supuesto que atraviesa todo el track. Los relojes lógicos, los vectores de versión y los relojes híbridos resuelven el problema del orden entre réplicas que cooperan. Una subasta, una bolsa o cualquier mercado con prioridad temporal no cumple ese supuesto: el cliente que emite el evento se beneficia de mentir sobre cuándo lo emitió. Ninguna técnica de ordenación distribuida sin coordinación es robusta frente a un emisor que controla su propio contador, y por eso los mercados se implementan como un libro de órdenes centralizado incluso cuando la latencia es el activo más caro del negocio. Si el sector que más dinero gasta en reducir latencia acepta un punto de serialización, la latencia no era el argumento.
Hay un patrón transversal a los cuatro que merece nombrarse porque explica por qué tantos equipos se equivocan en la estimación inicial. En todos ellos la invariante se comporta de forma benigna mientras el recurso abunda y se vuelve rígida exactamente cuando escasea, que es cuando llega el pico de tráfico. Un catálogo con mil unidades tolera cualquier optimismo; el mismo catálogo con tres unidades y un lanzamiento anunciado no tolera ninguno. La consecuencia es que las pruebas en condiciones normales nunca detectan el problema: el sistema funciona de maravilla durante meses y falla el día que más gente lo mira. Diseñar para el caso abundante y descubrir el caso escaso en producción es la trayectoria estándar de este error.
Converger no es lo mismo que ser correcto
El eslogan de los CRDT dice sin conflictos, y esa frase se lee mal casi siempre. Lo que un CRDT garantiza es convergencia fuerte eventual: dos réplicas que han visto el mismo conjunto de operaciones están en el mismo estado. Es una propiedad sobre el acuerdo, no sobre la validez. Un contador con incrementos y decrementos converge impecablemente a menos treinta si eso es lo que dicen las operaciones. La fusión está definida; la invariante del dominio, violada. Todas las réplicas coinciden en el mismo estado incorrecto, que es una forma particularmente sofisticada de estar equivocado.
// Convergencia perfecta, dominio destruido.
// Dos replicas, saldo inicial 100, ambas offline.
replicaA.aplicar({ tipo: 'retirada', importe: 80 }); // A ve saldo 20, valido
replicaB.aplicar({ tipo: 'retirada', importe: 80 }); // B ve saldo 20, valido
fusionar(replicaA, replicaB); // saldo -60 en ambas: coinciden, y es ilegal
Conviene además desmontar la lectura publicitaria del término conflicto. Cuando un motor de sincronización promete que no hay conflictos, lo que promete es que la fusión nunca falla, es decir, que el algoritmo siempre devuelve algo. No promete que ese algo sea lo que ninguno de los dos usuarios quería, ni que conserve la intención de ambos, ni desde luego que respete una regla de negocio. Hay tres propiedades distintas apiladas bajo la misma palabra —ausencia de fallo de fusión, preservación de intención y validez del dominio— y solo la primera está garantizada. Confundirlas es el origen de la mayoría de las decepciones con estas herramientas.
La razón profunda es que un tipo replicado sin conflicto no puede rechazar. Rechazar exige conocimiento del conjunto completo en el instante de la escritura, que es justo lo que la réplica no tiene. Existen construcciones que parecen contradecir esto —contadores acotados, patrones de fianza o escrow que reparten un presupuesto entre réplicas— y estudiarlas es instructivo porque revelan la trampa: reparten permiso por adelantado. La coordinación no se elimina, se adelanta en el tiempo y se amortiza. Funciona cuando el recurso es fungible y abundante; se degrada a coordinación pura cuando escasea, y es inaplicable cuando el recurso es único, porque un asiento no se puede dividir en presupuestos.
// El patron de fianza: coordinar una vez, decidir localmente despues.
// El coordinador reparte permiso por adelantado entre replicas.
const presupuesto = await coordinador.reservar({ recurso: 'unidades', cantidad: 50 });
// A partir de aqui la replica decide sola y sin red, pero solo hasta 50.
if (presupuesto.consumir(1)) {
confirmarVenta(); // decision local, instantanea, correcta
} else {
await coordinador.pedirMas(); // se acabo la holgura: vuelve la coordinacion
}
// La invariante se respeta porque nadie repartio mas permiso del que habia.
// El precio: hay stock inmovilizado en replicas que quiza no venden nada.
Ese fragmento resume la única forma honesta de comprar autonomía local sobre una invariante global: inmovilizar recurso. La réplica decide sin red porque alguien le entregó antes una porción exclusiva del derecho a decidir, y esa porción no está disponible para nadie más aunque no se use. Cuando el recurso abunda, el desperdicio es irrelevante y el patrón parece magia. Cuando escasea, el desperdicio es exactamente lo que no te puedes permitir, y el sistema degrada a pedir permiso en cada operación, que es el cliente-servidor del que se quería huir, ahora con más piezas.
Merece la pena extraer la moraleja general porque se aplica mucho más allá de los contadores. Toda técnica que promete decisión local sobre una invariante global funciona repartiendo autoridad por adelantado, y repartir autoridad por adelantado significa decidir antes con menos información. Eso es un intercambio, no una victoria: se gana latencia y disponibilidad, se pierde utilización del recurso y capacidad de reaccionar a lo que ocurra después del reparto. Un ingeniero que sabe nombrar ese intercambio puede negociarlo con el negocio; uno que cree haber eliminado la coordinación no puede ni explicar por qué hay existencias bloqueadas en un almacén que no vende.
No todas las invariantes valen lo mismo. Las aerolíneas sobrevenden asientos a propósito: han decidido que la violación es aceptable porque su coste es acotado, monetario y compensable en el mostrador. Ese es un cálculo legítimo. Deja de serlo cuando el coste es ilimitado, legal o reputacional: doble gasto, venta doble de un bien único, exposición de una posición en un mercado. Antes de elegir arquitectura, pon un número al fallo. Si no puedes ponerlo, ya tienes la respuesta.
Lo que sí queda para el cliente
Descartar el dato central no descarta la aplicación, y esta es la parte del argumento que los defensores del enfoque suelen conceder demasiado tarde y sus detractores nunca. Incluso en estos dominios existe una periferia amplia que es genuinamente del usuario y se beneficia entera del enfoque local: el borrador de una transferencia antes de firmarla, los filtros y la sesión de búsqueda de vuelos, el mapa de asientos que se explora, el carrito antes del pago, las notas internas de un gestor sobre un cliente, la lista de favoritos, el histórico ya liquidado que solo se lee. Ninguno de esos datos porta una invariante global, y todos se pueden hacer instantáneos, offline y duraderos sin comprometer nada.
Hay una asimetría útil dentro de estos dominios: el pasado ya no tiene invariantes vivas. Un movimiento bancario liquidado, una reserva ya consumida o una subasta cerrada son hechos inmutables que nadie va a volver a modificar, y por tanto se replican sin ningún riesgo de violación. La consecuencia práctica es que la parte más consultada de una aplicación financiera —revisar el histórico, buscar, filtrar, exportar— puede ser completamente local e instantánea aunque el núcleo transaccional sea rígidamente central. Curiosamente es la parte que casi siempre se deja como consulta al servidor con paginación lenta.
La regla que ordena esa separación es corta: local para la intención, central para el compromiso. La intención es del usuario, existe en su cabeza antes de existir en ningún servidor y merece ser instantánea. El compromiso es una afirmación del sistema sobre el mundo compartido y solo la puede emitir quien tiene autoridad. El error caro no es respetar esa frontera, sino difuminarla en la interfaz: mostrar transferencia realizada sobre un estado optimista y deshacerlo veinte minutos después no es una mejora de la latencia percibida, es una mentira con consecuencias. En estos dominios, una espera honesta vale más que una confirmación falsa, porque lo que se está construyendo con cada confirmación no es una pantalla sino confianza.
De ahí sale una exigencia de vocabulario que suele quedarse fuera de las conversaciones de arquitectura y que en realidad las cierra. La interfaz debe tener palabras distintas para tres estados que el usuario percibe como uno solo: guardado en este dispositivo, enviado al servidor y confirmado por el servidor. Un producto que colapsa los tres en un tic verde ha decidido que su modelo mental sea más simple que su arquitectura, y esa diferencia la acaba pagando alguien en soporte. Cuando los tres estados son visibles, el optimismo deja de ser una mentira y se convierte en lo que siempre debió ser: una promesa acotada y revisable.
Existe además una razón operativa para respetar esta frontera aunque técnicamente pudieras saltártela. En los dominios regulados, la reconstrucción de lo ocurrido es un requisito con nombre propio, y reconstruir exige un registro autorizado, ordenado y no repudiable de las decisiones. Un histórico causal fusionado desde varias réplicas puede ser matemáticamente elegante y aun así no servir como prueba, porque la pregunta del auditor no es en qué orden convergieron los nodos sino quién decidió qué y cuándo lo supo el sistema. Esa pregunta solo la responde una autoridad.
Queda una objeción legítima que conviene atender antes de cerrar, porque quien la formula suele tener razón. Se puede argumentar que las cadenas de bloques resuelven exactamente esto sin servidor, y en cierto sentido lo hacen: producen un orden total acordado entre participantes que no confían entre sí. Pero el precio de esa proeza es informativo, porque consiste en pagar coordinación de forma explícita, costosa y lenta en cada escritura, con un mecanismo de consenso que existe precisamente porque el problema no admite atajos. No es un contraejemplo a esta lección: es su confirmación más cara. Cuando la invariante global es el producto, alguien paga coordinación, y la única decisión de diseño disponible es quién la paga y con qué moneda.
El salto conceptual de esta lección es dejar de preguntar qué arquitectura quiero y empezar a preguntar qué predicado tiene que ser verdad siempre, porque ese predicado ya contiene la respuesta antes de que elijas herramienta. Una invariante confluente —fusionar dos estados válidos da un estado válido— es replicable, y ahí local-first no solo cabe: es superior, porque elimina un viaje de red que no aportaba nada. Una invariante no confluente exige un punto donde una escritura pueda ser rechazada, y ese punto es lo que llamamos servidor; puedes llamarlo líder, coordinador, secuenciador o contrato en cadena, pero habrás reinventado la centralización con otro nombre y probablemente con peores garantías. La consecuencia práctica no es abandonar el enfoque, sino invertir el orden del diseño: primero inventarias las invariantes del dominio y las clasificas, después decides que la mayoría son locales y las liberas, y por último aíslas el núcleo no confluente en la superficie más pequeña posible. Los sistemas distribuidos maduros no eligen entre coordinación y ausencia de coordinación: eligen exactamente dónde pagarla, y cobran esa decisión en el resto de la aplicación. Quien entiende esto deja de discutir sobre motores de sincronización y empieza a discutir sobre el dominio, que es donde estaba el problema desde el principio.
- Escribe las cinco invariantes más importantes de un producto que conozcas, cada una en una sola frase y sin jerga técnica.
- Clasifícalas: para cada una, decide si la fusión de dos estados válidos siempre da un estado válido. Justifica los casos dudosos con un contraejemplo concreto.
- Para las no confluentes, pon un número al coste de una violación: euros, horas de soporte, riesgo legal. Ordénalas por ese número.
- Elige la invariante más cara y diseña un reparto de permiso por adelantado. Calcula cuánto recurso quedaría inmovilizado con diez réplicas y decide si compensa.
- Dibuja la frontera resultante y cuenta qué porcentaje de las pantallas del producto queda al lado local. Si es menos de la mitad, argumenta si el proyecto justifica un motor de sincronización.