El patrón del lienzo de diseño: por qué no es local-first aunque lo parezca
Una herramienta de diseño en tiempo real puede trabajar sin conexión durante días y aun así no ser local-first; el caso mejor documentado explica qué eligieron en su lugar y por qué fue la decisión correcta.
Un lienzo de diseño colaborativo cumple, a primera vista, todos los síntomas del ideal: responde al instante, muestra los cursores de tus compañeros, aguanta sin conexión el tiempo que haga falta y no pierde tu trabajo cuando vuelves. Si aplicaras la lista de comprobación superficial, marcarías todas las casillas. Y sin embargo el equipo que construyó el caso mejor documentado de esta categoría escribió, con todas las letras y en su propio blog de ingeniería, que su servidor es la autoridad central y que por eso pudieron simplificar el sistema. Esta lección es la más útil del nivel precisamente porque estudia un producto excelente que decidió no ser local-first, explicó por qué, y acertó. Aprender a leer esa decisión vale más que cualquier catálogo de estructuras convergentes.
- Distinguir trabajar sin conexión de tener la autoridad sobre el dato, que es el eje del nivel 2.
- Analizar una renuncia explícita y razonada tanto a la transformación operacional como a los CRDT.
- Entender qué se gana y qué se pierde al conservar un árbitro central en un editor gráfico.
- Reconocer la partición del producto en dos sistemas de sincronización con garantías distintas.
La fuente: una renuncia doble, escrita y firmada
El material de esta lección procede de un único artículo, publicado por Figma en octubre de 2019 y firmado por Evan Wallace, cofundador y en su momento director técnico de la empresa. Es una fuente primaria, extensa y técnicamente concreta, y su valor está en que no describe solo lo que hicieron sino lo que descartaron. La primera renuncia es a la transformación operacional: la consideraron innecesariamente compleja para su problema y explicaron el motivo con franqueza poco habitual, que como empresa joven valoraban poder enviar funcionalidades rápido y que aquello era desproporcionado. Añaden que la transformación operacional es excelente para textos largos con poca sobrecarga de memoria, pero muy difícil de implementar correctamente.
La segunda renuncia es la que importa para este track. Tras explicar qué son los tipos replicados sin conflictos, el artículo dice literalmente que Figma no usa CRDT de verdad, y da la razón exacta: los CRDT están diseñados para sistemas descentralizados donde no hay una autoridad única que decida el estado final, eso tiene un coste inevitable de rendimiento y memoria, y como Figma está centralizado —su servidor es la autoridad central— pueden retirar esa sobrecarga y quedarse con una implementación más rápida y ligera. Precisan además que su estructura no es un CRDT sino algo inspirado en varios CRDT distintos combinados.
La justificación no es los CRDT son difíciles ni no los entendíamos. Es que el coste de los CRDT compra una propiedad que ellos no necesitaban: funcionar sin árbitro. Quien tiene árbitro y paga igualmente ese coste está comprando un seguro que no va a usar. Es el razonamiento inverso al que suele hacerse en las discusiones de arquitectura, donde se elige la técnica más general por si acaso, y es la razón por la que este caso enseña más que muchos éxitos del bando contrario.
El documento como mapa de dos niveles
La estructura de datos que describen es de una simplicidad casi provocadora. Un documento es un árbol de objetos, similar al modelo de objetos del navegador: una raíz, páginas debajo, y una jerarquía de objetos dentro de cada página. Cada objeto tiene un identificador y una colección de propiedades con valores, y el artículo ofrece dos formas equivalentes de pensarlo: como un mapa de dos niveles del tipo Map<ObjectID, Map<Property, Value>>, o como una base de datos con filas que guardan ternas de identificador de objeto, propiedad y valor. De ahí se sigue algo que ellos señalan de pasada y que explica su velocidad de producto: añadir una funcionalidad nueva suele consistir sencillamente en añadir propiedades nuevas a los objetos.
Sobre esa estructura, la regla de sincronización cabe en una frase: el servidor recuerda el último valor que cualquier cliente le haya enviado para una propiedad dada de un objeto dado. Dos clientes tocando propiedades distintas del mismo objeto no chocan; dos clientes tocando la misma propiedad de objetos distintos tampoco. El conflicto solo existe cuando coinciden objeto y propiedad, y entonces queda el último valor que llegó al servidor. El artículo lo compara con un registro de última escritura de la literatura de CRDT y añade la diferencia decisiva: no necesitan marca temporal, porque el servidor puede definir el orden de los sucesos.
flowchart TD C1[Cliente uno cambia color] --> S[Servidor arbitro] C2[Cliente dos cambia posicion] --> S C3[Cliente tres cambia color a la vez] --> S S --> R[Guarda el ultimo valor por objeto y propiedad] R --> B[Difunde a todos] B --> L[Los clientes descartan lo entrante que choque con lo suyo sin confirmar] style S fill:#f38ba8,color:#11111b style L fill:#89b4fa,color:#11111b
Esa regla trae una consecuencia que el propio artículo declara sin disimulo: los cambios son atómicos en la frontera del valor de la propiedad, y el valor final siempre es uno de los que algún cliente envió. Por eso —lo dicen ellos— editar simultáneamente el mismo texto no funciona: si el valor era B y alguien lo cambia a AB mientras otro lo cambia a BC, el resultado será AB o BC, nunca ABC. Y la justificación es de producto, no de ingeniería: Figma es una herramienta de diseño, no un editor de texto, y ese caso de uso no está entre los que optimizan.
Las cuatro decisiones que sostienen el conjunto
Vale la pena recorrer los cuatro problemas concretos que resolvieron, porque cada uno es una lección aparte y todos están documentados con detalle.
El parpadeo aparece porque los cambios locales se aplican de inmediato, sin esperar confirmación, para que la herramienta se sienta ágil. Si además se aplicaran todos los cambios entrantes según llegan, un valor confirmado más antiguo sobrescribiría temporalmente uno propio más reciente y la pantalla titilaría. Su solución es descartar los cambios entrantes que choquen con propiedades propias todavía sin confirmar, con el argumento de que el cambio propio es la mejor predicción del valor finalmente consistente por ser el más reciente en el orden de llegada al servidor.
Los identificadores los genera el cliente, combinando un identificador único de cliente con el del objeto nuevo, y el artículo explica por qué no puede hacerlo el servidor: la creación de objetos tiene que funcionar sin conexión. Es exactamente el argumento del nivel 22 sobre identidades sin coordinación, aplicado por una razón de producto y no de teoría.
El borrado elimina del servidor todos los datos del objeto, y las propiedades borradas se guardan en el búfer de deshacer del cliente que borró, que es entonces el responsable de restaurarlas si alguien deshace. La motivación que dan es evitar que los documentos de larga vida crezcan indefinidamente conforme se editan: es una solución al problema de las lápidas que solo es posible porque hay un árbitro y alguien puede decidir que ese dato ya no existe para nadie.
El reparentado es el que reconocen como más complicado. Guardan el vínculo al padre como una propiedad del hijo, lo que preserva la identidad del objeto y evita que algo acabe con dos padres. El precio es que esos vínculos son aristas dirigidas de un grafo y nada garantiza que formen un árbol: si un cliente hace A hijo de B mientras otro hace B hijo de A, hay ciclo. El servidor rechaza las actualizaciones que lo producirían, pero el cliente no puede rechazar nada porque el servidor manda, así que puede quedarse transitoriamente con un ciclo; su solución es emparentar esos objetos entre sí y sacarlos del árbol hasta que el servidor rechace el cambio. Lo califican de solución no muy buena para un problema raro y temporal, y prefieren eso a complicar el cliente. El orden entre hermanos lo resuelven con indexación fraccionaria, y advierten que el vínculo al padre y la posición deben guardarse como una sola propiedad para que se actualicen a la vez.
Autoridad central
El servidor define el orden de los sucesos. Eso ahorra relojes, ahorra metadatos y permite borrar de verdad.
Atomicidad por propiedad
El valor final es siempre uno de los enviados. Nunca se fabrica un valor intermedio, y el texto no se fusiona.
Identificadores del cliente
Se generan en local porque crear objetos debe funcionar sin conexión. Es la única concesión inevitable.
Árbol vigilado
El vínculo al padre es propiedad del hijo, el servidor rechaza ciclos y el cliente los esconde mientras tanto.
Dos sistemas, no uno, y qué pasa al volver
Queda el detalle que decide la clasificación del producto y que suele pasarse por alto al citar este artículo. Figma usa su sistema multijugador solo para los documentos. Los comentarios, los usuarios, los equipos y los proyectos viven en PostgreSQL, no en el sistema multijugador, y se sincronizan con los clientes mediante un sistema completamente distinto; el artículo dice que las dos implementaciones son parecidas pero están separadas por compromisos diferentes en rendimiento, disponibilidad sin conexión y seguridad. Traducido: la parte que se comporta como local-first es el lienzo, y la parte que gobierna quién puede ver qué no lo hace.
Y sobre la desconexión, el artículo es igual de explícito. Puedes estar sin conexión un tiempo arbitrario y seguir editando; al volver, el cliente descarga una copia nueva del documento y reaplica encima las ediciones hechas sin conexión, y a partir de ahí sigue sincronizando por una conexión nueva. Ese es el criterio decisivo del nivel 2 aplicado a este caso: la copia local es un caché de trabajo excelente con cola de escrituras pendientes, no una réplica autosuficiente, porque la reincorporación pasa por traerse la verdad de fuera. Generalizar esto a todas las herramientas de diseño en tiempo real sería una inferencia mía y no un hecho documentado; lo que sí es un hecho es que el único caso de esta categoría con una explicación pública de este nivel de detalle eligió esta arquitectura y la defendió.
Vale la pena detenerse aquí porque este caso disuelve, si se lee con atención, la discusión más estéril de todo el campo. La costumbre es tratar local-first como una etiqueta binaria y preguntar de cada producto si la merece, lo cual produce debates interminables y ninguna decisión de ingeniería. El artículo que hemos leído sugiere una pregunta infinitamente más productiva y que además se puede contestar con datos: enumera las operaciones de tu producto y decide, una por una, si tienen que poder completarse sin que exista ningún servidor en el mundo. Cuando haces ese ejercicio con un lienzo de diseño, el resultado es revelador. Mover un rectángulo, cambiar un color, crear un objeto: tienen que funcionar solas, y funcionan, y por eso hicieron falta identificadores generados en el cliente y aplicación optimista inmediata. Ordenar hermanos: tiene que funcionar sola, y por eso hay indexación fraccionaria. Saber si María puede abrir este fichero: no tiene que funcionar sola, jamás, porque una respuesta local a esa pregunta es un fallo de seguridad y no una funcionalidad. Decidir qué versión del documento es la buena cuando dos personas volvieron de un vuelo distinto: podría funcionar sola, con bastante maquinaria, y ellos decidieron que no valía la pena porque el árbitro ya existía por otras razones. Fíjate en que la respuesta a esa pregunta, operación por operación, es la arquitectura: no hay que elegir un bando y después derivar el diseño, hay que responder la lista y el diseño sale solo. Y advierte la asimetría que este caso hace visible y que ninguna otra lección del nivel muestra tan bien: las operaciones que deben funcionar solas empujan hacia identidades sin coordinación y estructuras convergentes, mientras que las operaciones que no deben funcionar solas —autorización, facturación, invitaciones, cualquier cosa donde la respuesta correcta dependa de una política que otros pueden cambiar— empujan hacia un árbitro, y esas dos fuerzas no se resuelven eligiendo una, se resuelven partiendo el producto en dos sistemas con garantías distintas, que es exactamente lo que hicieron. El error que este caso previene, y que he visto arruinar proyectos enteros, es intentar meter la segunda categoría dentro de la primera: construir permisos convergentes, listas de miembros que se fusionan y cuotas que se reconcilian, porque la etiqueta elegida obligaba a que todo fuese local. No obliga. Un producto puede ser radicalmente local en su núcleo creativo y descaradamente centralizado en su periferia administrativa, y no solo es aceptable: en presencia de varios usuarios con derechos distintos, probablemente es lo único correcto.
- Enumera todas las operaciones de tu aplicación, incluidas las administrativas que sueles olvidar: invitar, revocar, cambiar de plan, exportar.
- Marca cada una con si debe completarse sin ningún servidor disponible, y anota al lado qué consecuencia tendría equivocarse en cada dirección.
- Comprueba si alguna operación de la lista de las que no deben funcionar solas está hoy implementada con una estructura convergente, y calcula qué garantía estás fingiendo.
- Para el núcleo creativo, decide si necesitas fusión a nivel de carácter o te basta con atomicidad por propiedad, y sé honesto sobre lo que tu producto es.
- Si tienes árbitro, aprovéchalo: elimina de tu diseño los relojes y los metadatos causales que solo servían para prescindir de él.
- Documenta por escrito qué ocurre exactamente al reincorporarse tras una desconexión larga, y si tu cliente puede reconstruir el estado sin descargarlo de nuevo.