Sync-first y las apps con caché optimista
Las mutaciones optimistas y su reversión al fallar producen una experiencia indistinguible de local-first desde fuera, pero el estado optimista es una predicción sujeta al veto del servidor, no una verdad que el cliente pueda prometer.
Llegamos a la categoría más resbaladiza de las cuatro, y también a la más extendida en el software que usas todos los días. Una aplicación con caché optimista aplica tus cambios en la interfaz antes de que nadie los haya autorizado, y solo da marcha atrás si el servidor se niega. El resultado, desde el asiento del usuario, es exactamente la sensación que el manifiesto local-first prometía: pulsas y ocurre, sin ruleta, sin espera, sin señal alguna de que haya una red por medio. Y precisamente por eso este patrón se ha convertido en el gran productor de confusión taxonómica del sector, hasta el punto de que se ha acuñado un término —sync-first— que suena a categoría arquitectónica y en realidad solo describe una forma de mover bytes. Esta lección disecciona el patrón, aísla el punto exacto donde deja de ser local-first, y explica por qué la evidencia visual es aquí un testigo poco fiable.
- Descomponer la mutación optimista en sus tres estados y entender cuál de ellos es un hecho.
- Reconocer la reversión como la firma inequívoca de una autoridad que reside en otro sitio.
- Situar el término sync-first en la capa que le corresponde: transporte, no autoridad.
- Explicar por qué la experiencia de usuario no permite distinguir estas arquitecturas desde fuera.
- Diseñar el manejo del rechazo de forma que la reversión no se convierta en pérdida de datos.
La mutación optimista es una predicción
El patrón tiene una anatomía muy regular, y verla escrita ayuda a no confundirla con otra cosa. El usuario ejecuta una acción. La aplicación calcula cuál sería el estado si esa acción tuviera éxito y lo pinta de inmediato, manteniendo aparte el estado confirmado que tenía antes. Envía la petición. Si vuelve bien, promueve el estado optimista a confirmado y tira el respaldo. Si vuelve mal, descarta el estado optimista, restaura el respaldo y avisa al usuario, o no le avisa, que es lo habitual.
Nótese que la anatomía exige mantener dos versiones del mismo estado a la vez, y que esa duplicidad es el rasgo estructural definitorio del patrón. Existe un estado confirmado, que es lo que la autoridad ha ratificado, y un estado presentado, que es el confirmado con las predicciones pendientes aplicadas encima. La interfaz lee siempre el segundo y el sistema razona siempre con el primero. Toda la dificultad de este patrón nace de mantener esas dos capas coherentes entre sí mientras llegan confirmaciones, rechazos y cambios ajenos en orden arbitrario.
Que este patrón se haya vuelto omnipresente tiene una explicación técnica y una psicológica, y ambas merecen registrarse. La técnica es que la latencia de ida y vuelta es incompresible por debajo de un suelo físico, de modo que la única forma de que una interfaz responda en el umbral de lo instantáneo es no esperar la respuesta. La psicológica es que la percepción de causalidad directa entre una acción y su efecto se degrada con extraordinaria rapidez: por encima de unas pocas decenas de milisegundos, el usuario deja de sentir que ha manipulado un objeto y empieza a sentir que ha enviado una solicitud. El patrón optimista existe para preservar esa sensación de manipulación directa, y por eso funciona tan bien.
Lo importante es lo que ese esquema implica sobre la naturaleza de lo que se muestra en pantalla durante la ventana intermedia. No es un hecho: es una predicción sobre el comportamiento futuro de una autoridad ajena. La aplicación está apostando a que el servidor dirá que sí, con muy buena información y una tasa de acierto que suele superar el noventa y nueve por ciento, pero apostando. Y una apuesta con un noventa y nueve por ciento de acierto sigue siendo una apuesta, con la particularidad de que el uno por ciento restante aparece justo cuando más importa: en conflictos de colaboración, en cambios de permisos, en reglas de negocio que se activan por casos raros.
stateDiagram-v2 [*] --> confirmado confirmado --> optimista: el usuario actua optimista --> confirmado: el servidor acepta optimista --> revertido: el servidor rechaza revertido --> confirmado: se restaura el respaldo
El diagrama omite deliberadamente una arista que existe en todos los sistemas reales y que conviene tener presente: la que va de optimista a optimista, cuando llega un cambio ajeno sobre el mismo dato mientras tu predicción sigue pendiente. Esa transición obliga a recalcular la predicción sobre una base distinta de la que se emitió, y es donde se concentran los errores más difíciles de reproducir de este patrón, porque dependen de un orden de llegada que solo se da bajo condiciones de red concretas.
Mira el diagrama y localiza el único estado que corresponde a un hecho del mundo. Es confirmado, y se alcanza siempre pasando por una decisión ajena. El estado optimista es transitorio por construcción: no tiene existencia propia, solo la de una hipótesis esperando veredicto. En una arquitectura donde el cliente fuera autoritativo, ese diagrama tendría un solo estado y ninguna arista de rechazo, porque no habría nadie con potestad para emitirla.
Conviene además notar una asimetría temporal incómoda. La ventana entre la acción y la confirmación se dimensiona mentalmente como decenas de milisegundos, y con red buena lo es. Pero si el patrón se combina con una cola de escrituras sin conexión, esa ventana puede durar horas o días, y durante todo ese tiempo la interfaz muestra como si fueran hechos un conjunto de predicciones que envejecen. La aplicación honesta marca visualmente lo no confirmado; la aplicación cómoda no lo marca y confía en que el uno por ciento no le toque delante de un usuario importante.
Hay además un efecto de composición que agrava el problema y que casi nadie modela. Las predicciones se apilan: el usuario crea un elemento no confirmado, lo edita, lo mueve dentro de una colección y lo comparte, y ahora hay cuatro operaciones encadenadas cuya validez depende de que la primera sea aceptada. Si el servidor rechaza la creación, no hay una reversión sino una cascada, y cada eslabón puede requerir un tratamiento distinto. La complejidad de la reversión crece con la profundidad de la cadena, y es precisamente esa complejidad la que empuja a muchos equipos a la salida fácil de deshacer todo en silencio.
El derecho de veto y la reversión
Conviene además separar dos causas de rechazo que suelen tratarse igual y no lo son. Una es el rechazo por invalidez: la operación infringe una regla, el usuario ya no tiene permiso, el recurso desapareció. La otra es el rechazo por concurrencia: la operación era válida cuando se emitió y dejó de serlo porque alguien tocó lo mismo mientras tanto. La primera exige informar y no reintentar; la segunda admite a menudo una fusión o un reintento sobre el estado nuevo. Tratar ambas con la misma reversión genérica descarta trabajo que era perfectamente recuperable.
Si tuviera que darte un único detector, sería este: busca la ruta de código que deshace. Si existe un camino por el que el trabajo del usuario puede desaparecer porque una parte remota lo rechazó, el cliente no es autoritativo, y ninguna cantidad de almacenamiento local, de arranque instantáneo o de sincronización en segundo plano cambia esa conclusión. La reversión es la firma de la autoridad ajena; es la prueba documental de que había un tribunal.
Hay un fallo de diseño casi universal en las implementaciones de este patrón: el rechazo se maneja restaurando el estado anterior y, como mucho, mostrando un aviso efímero. Ese aviso desaparece en tres segundos, y a menudo llega cuando el usuario ya está en otra pantalla o ha cerrado el portátil. El resultado es una clase de pérdida de datos que no aparece en ningún registro de errores y que el usuario descubre semanas después sin poder reconstruir qué pasó. Si tu arquitectura tiene derecho de veto, la obligación mínima es que el veto sea inescapable para el usuario y que su contenido —lo que él escribió— se conserve en algún sitio recuperable. Deshacer en silencio no es una degradación elegante, es una pérdida.
Del detector se derivan tres exigencias mínimas para cualquier sistema que conserve el veto, y merece la pena enunciarlas como requisitos y no como buenas prácticas.
El rechazo debe ser inescapable
Un aviso que caduca en tres segundos no es una notificación, es una formalidad. Si el sistema puede deshacer trabajo del usuario, la comunicación de ese hecho debe persistir hasta que él la reconozca, sobrevivir a un cierre de la aplicación y ser localizable después.
Lo revocado debe conservarse
Deshacer no puede significar destruir. El contenido rechazado debe quedar en algún sitio recuperable —un borrador, un historial, un archivo local— para que el usuario pueda reintentar, copiar o al menos comprobar qué había escrito. La reversión que no conserva es pérdida.
El motivo debe ser legible
Rechazado no es un motivo. El usuario necesita saber si chocó con otra edición, si perdió un permiso o si infringió una regla, porque cada caso pide una acción distinta. Un veto sin razón obliga a adivinar, y adivinar mal cuesta más trabajo que el que se perdió.
Existe una variante que confunde todavía más porque parece devolver la autoridad al cliente: la que reejecuta las mutaciones del cliente en el servidor y solo rechaza cuando la comprobación falla. El motor de sincronización Zero, de Rocicorp, funciona explícitamente así, y su documentación es notablemente franca al respecto: el servidor vuelve a aplicar la mutación contra su propio estado y tiene la última palabra, de modo que el resultado local es provisional hasta ese momento. Esa franqueza es valiosa y poco común, y merece leerse como lo que es: una declaración deliberada de dónde está la autoridad, no una carencia. Un motor que coloca la autoridad en el servidor está eligiendo poder garantizar invariantes globales y permisos que ninguna arquitectura sin árbitro puede prometer.
Lo que hace confusa a esta variante es que la lógica de la mutación se escribe una sola vez y se ejecuta en los dos lados, lo que produce la impresión de simetría entre cliente y servidor. La simetría es real en el código y falsa en la autoridad: la ejecución local es una simulación cuyo propósito es adelantar el resultado, y la remota es la que cuenta. Es una arquitectura elegante precisamente porque hace que la simulación sea fiel sin ambigüedad sobre quién decide, y confundir su elegancia con soberanía del cliente sería malinterpretar lo que sus autores dicen con claridad.
Sync-first describe el transporte
Con esto en la mano, el término sync-first se coloca solo. Lo que nombra es un cambio en cómo la aplicación obtiene y envía datos: en lugar de que cada pantalla dispare peticiones puntuales y gestione su propio estado de carga, hay un motor que mantiene un subconjunto de datos replicado y actualizado de forma continua, y la interfaz simplemente lee de esa réplica local como si fuera una base de datos.
Conviene apreciar la magnitud del cambio antes de relativizarlo, porque es genuinamente grande. Sustituye un modelo mental —cada pantalla es responsable de pedir lo suyo, gestionar su carga, su error y su recarga— por otro radicalmente distinto: existe un conjunto de datos vivo del que la interfaz es una función pura. Esa reformulación elimina de golpe la mayor parte del código accidental de una aplicación conectada y hace que las actualizaciones en vivo dejen de ser una funcionalidad que se implementa para pasar a ser una consecuencia de la arquitectura.
Es un cambio real y muy provechoso. Elimina la ruleta de carga de casi toda la aplicación, unifica el manejo de la asincronía en un solo lugar, hace triviales las actualizaciones en vivo y reduce a la nada una clase entera de errores de sincronización manual. Pero es un cambio en la capa de transporte y de acceso a datos. No dice nada sobre quién decide. Un motor de sincronización puede empujar tablas hacia el cliente y seguir exigiendo que toda escritura pase por el camino tradicional y sea juzgada por el servidor; ElectricSQL, tras su rediseño de 2024, hace exactamente eso —transmite en continuo porciones de una base Postgres al cliente sin exigirle a este que use estructuras de datos convergentes— y es una arquitectura perfectamente coherente. La lectura se replica; la escritura sigue teniendo dueño.
Lo que sync-first sí afirma
Que existe una réplica local viva y actualizada de forma continua, que la interfaz consulta localmente en vez de pedir por pantalla, y que la propagación de cambios es responsabilidad de un motor y no del código de producto.
Lo que sync-first no afirma
Nada sobre quién resuelve un conflicto, ni sobre si tus escrituras pueden ser revocadas, ni sobre si tus datos siguen siendo legibles sin el proveedor, ni sobre si la réplica local contiene el conjunto completo o solo una porción autorizada.
Cómo comprobarlo en diez minutos
Corta la red y escribe. Reconecta desde otra sesión y escribe lo mismo. Observa cuál de las dos versiones sobrevive y si alguien te lo explica. La respuesta a esa pregunta sitúa el motor en el eje mejor que cualquier página de documentación.
Que un término describa la capa de transporte no lo invalida; lo que lo hace peligroso es usarlo como si describiera la de autoridad. Y ocurre constantemente, porque sync-first suena más cercano a local-first que a caché, y la cercanía fonética hace un trabajo retórico que la técnica no respalda.
Hay una pregunta que desarma cualquier ambigüedad al evaluar un motor de sincronización, y conviene hacerla antes que ninguna otra sobre rendimiento o ergonomía: ¿qué ocurre exactamente con una escritura local cuando el servidor la rechaza? Si la respuesta describe una reversión, el motor coloca la autoridad en el servidor y lo que ofrece es una réplica de lectura con escritura optimista. Si la respuesta es que eso no puede ocurrir porque el servidor no juzga, la autoridad está en el cliente. No hay una tercera respuesta posible, y la claridad con que un proyecto la da suele ser un indicador fiable de la calidad de su documentación en general.
Una segunda pregunta, casi tan reveladora, es qué porción del conjunto de datos llega al dispositivo y quién decide esa porción. Un motor que sincroniza subconjuntos definidos por consultas está tomando una decisión de autoridad encubierta: quien define la porción define lo que puedes ver sin red, y esa definición suele vivir en el servidor. Un producto puede tener una réplica local viva y aun así depender por completo de que alguien remoto decida cada mañana qué entra en ella.
Por qué la experiencia engaña
Queda por explicar el fondo del problema, que es epistemológico antes que técnico. La experiencia de usuario no contiene información suficiente para distinguir estas arquitecturas, y no por un defecto de las implementaciones sino por diseño: todo el propósito del patrón optimista es que sea indistinguible de la escritura autoritativa. Si el usuario notara la diferencia, la técnica habría fracasado.
Las señales que la gente usa para clasificar —velocidad de respuesta, ausencia de indicadores de carga, funcionamiento sin red, actualización en vivo entre pestañas— son todas producibles por una caché suficientemente buena. Son señales de calidad de implementación, no de arquitectura de datos. Y las señales que sí discriminarían solo aparecen en dos momentos que el uso normal casi nunca provoca: la edición simultánea genuina del mismo dato por dos personas, y la desaparición del proveedor. Un producto puede pasar años sin exponer a un usuario típico a ninguno de los dos.
Esa rareza tiene un efecto perverso sobre la calidad del propio código de reversión. Las rutas que se ejecutan raramente son las que menos pruebas tienen, las que peor se entienden y las que más se degradan con cada refactorización, porque nada las ejercita. De modo que el camino que decide qué pasa con el trabajo del usuario en el peor momento suele ser, en la mayoría de los sistemas, el peor mantenido de todo el producto. Cualquier equipo que conserve el derecho de veto debería tratar ese camino como parte del recorrido principal, con pruebas que provoquen rechazos de forma deliberada y regular.
Hay una excepción parcial que vale la pena registrar, porque es la única señal observable que sí discrimina: la forma en que el producto trata lo no confirmado. Una aplicación que distingue visualmente entre lo escrito y lo ratificado —con un estado intermedio explícito, un indicador de pendiente, un registro consultable de lo que aún no ha sido aceptado— está admitiendo que existe una autoridad externa. Una que no distingue nada puede ser cualquiera de las dos cosas. La honestidad de la interfaz es, paradójicamente, la única pista arquitectónica que el usuario puede leer, y es una pista que solo aparece cuando alguien decidió darla.
De ahí se sigue una regla de higiene profesional: la clasificación se hace leyendo el modelo, no usando el producto. Preguntas dónde se resuelve un conflicto, qué ocurre con una escritura rechazada, si la réplica local es completa o parcial y qué queda si el servicio cierra. Son cuatro preguntas sobre el diseño, y ninguna se responde mirando la pantalla.
Merece un último apunte el hecho de que esta indistinguibilidad no es simétrica en sus consecuencias. Un sistema local-first descrito con modestia pierde una oportunidad comercial; un sistema con veto descrito como soberano crea una expectativa que su arquitectura no puede honrar, y esa expectativa se convierte en la explicación que el usuario se dará a sí mismo cuando pierda trabajo. La asimetría del daño es la razón por la que la carga de la precisión recae sobre quien reclama la etiqueta más fuerte.
Lo que la interfaz muestra durante una mutación optimista tiene la misma estructura lógica que una promesa condicionada: se cumplirá si el servidor la ratifica. La aplicación la pinta con el mismo aspecto que un hecho consumado porque esa es la fuente de su comodidad, pero al hacerlo introduce una divergencia deliberada entre lo que el usuario cree y lo que el sistema sabe, y esa divergencia es el terreno donde crecen los fallos más difíciles de este patrón. El usuario cierra el portátil convencido de haber guardado; el sistema sabe que había una condición pendiente y no tenía forma de decírselo. Un cliente autoritativo no vive esa tensión, no porque sea técnicamente superior sino porque ha aceptado un compromiso distinto: renuncia a garantizar invariantes globales a cambio de poder prometerle al usuario que lo escrito está escrito. Eso convierte la elección en un intercambio explícito y no en una escala de calidad. Un servidor autoritativo con mutaciones optimistas es la arquitectura correcta cuando hay reglas de negocio duras que alguien debe hacer cumplir, cuando los permisos son finos y cambiantes, o cuando la corrección global importa más que la soberanía del dato; el precio que paga es que el veto existe y hay que diseñarlo con dignidad. Lo que no es defendible es cobrar el precio y no prestar el servicio: presentarse como local-first, vender propiedad del dato, y conservar en silencio el derecho a deshacer el trabajo del usuario. La honestidad taxonómica no es un ejercicio de pureza terminológica; es lo que permite que un equipo sepa qué está prometiendo y que un usuario sepa qué está recibiendo.
- Toma un producto con caché optimista y provoca un rechazo real: edita el mismo registro desde dos sesiones a la vez, o retírate un permiso desde otra cuenta y luego intenta escribir.
- Observa qué hace la interfaz al recibir el rechazo. Cronometra cuánto tiempo está visible el aviso, si es que hay alguno, y comprueba si el contenido que escribiste se conserva en algún sitio recuperable.
- Repite el experimento con la red cortada y la escritura encolada. Mide cuánto tiempo transcurre entre tu acción y el veredicto. Ese intervalo es la ventana en la que la interfaz te está mostrando predicciones con cara de hechos.
- Escribe en dos frases cómo rediseñarías el manejo del rechazo para que la pérdida sea imposible: dónde guardarías lo revocado y cómo se lo presentarías al usuario sin interrumpirlo.
- Encadena cuatro acciones dependientes sin red y provoca el rechazo de la primera. Observa si el sistema revierte en cascada, si lo hace en el orden correcto y si conserva algo de las tres siguientes.
- Busca en la documentación del motor de sincronización que uses la frase que describe qué pasa con una escritura rechazada. Si no existe esa frase, has encontrado el hueco más importante de esa documentación.