El coste de ingeniería honesto
Lo que el enfoque local-first añade de verdad a un proyecto: piezas nuevas, un espacio de estados multiplicado, fallos irreproducibles y un impuesto permanente sobre cada funcionalidad futura.
Las tres lecciones anteriores han argumentado contra el enfoque desde el dominio: hay invariantes que no se replican, datos que no pueden bajar y permisos que no se pueden retirar. Esta argumenta desde un sitio distinto y, para la mayoría de los equipos, más decisivo: aunque tu dominio lo permita todo, el enfoque cuesta, y el coste tiene una forma traicionera. Es bajo en la demostración, moderado en el prototipo y creciente durante el resto de la vida del producto, porque no es un trabajo que se hace una vez sino un impuesto que se paga en cada funcionalidad nueva. Contarlo con honestidad es incómodo para quien defiende la arquitectura y necesario para quien va a mantenerla cinco años, y por eso casi nunca aparece en las charlas.
- Enumerar las piezas que aparecen en el proyecto y que antes no existían.
- Entender por qué el espacio de estados se multiplica y qué exige eso de las pruebas.
- Reconocer la clase de fallos que no se pueden reproducir y qué instrumentación los hace tratables.
- Hacer el cálculo honesto para un equipo pequeño antes de comprometerse.
Las piezas nuevas que aparecen en tu proyecto
Una aplicación cliente-servidor convencional tiene una fuente de verdad, un esquema, un despliegue y una operación de migración que ocurre una vez y de forma atómica. Un sistema replicado sustituye cada uno de esos singulares por un plural, y el plural trae compañía. Aparece un almacén local con su modelo de transacciones, un motor de sincronización con su protocolo y su versionado, una política de fusión explícita para cada tipo de dato, coordinación entre pestañas con elección de líder, gestión de cuota y recuperación tras desalojo, autenticación que funcione sin red, y sincronización en segundo plano con sus propias reglas de reintento.
| Pieza | En cliente-servidor | En un sistema replicado |
|---|---|---|
| Fuente de verdad | Una, en el servidor | Una por dispositivo, más el servidor |
| Migración de esquema | Atómica y con final | Despliegue distribuido que nunca termina del todo |
| Concurrencia | Transacciones de base de datos | Semántica de fusión decidida campo a campo |
| Sesión y permisos | Verificados en cada petición | Verificados en la entrega, y después ya no |
| Observabilidad | Registros y consultas del servidor | Herramientas propias de exportación desde el cliente |
| Protocolo | Interno y versionado a tu ritmo | Público entre versiones que no controlas |
Cada fila de esa tabla es una decisión de diseño que en la columna izquierda venía resuelta por defecto y en la derecha hay que tomar, documentar y sostener. Ese es el sentido preciso de la afirmación de que el enfoque duplica la superficie de ingeniería: no es que el código sea el doble de largo, es que el número de decisiones abiertas se dispara y las decisiones abiertas son lo que consume a un equipo.
De todas ellas, la que más subestiman los equipos es la migración de esquema. En el servidor, migrar es una operación con principio y final: se ejecuta, se verifica y el esquema antiguo deja de existir. En el cliente, migrar es un despliegue distribuido sobre dispositivos que no controlas, que pueden estar apagados durante meses y cuyos usuarios pueden negarse a actualizar. La consecuencia es que el código debe entender simultáneamente varias versiones del esquema durante un periodo que no puedes acotar, y que el protocolo de sincronización debe tolerar que dos extremos hablen versiones distintas. Es la clase de restricción que no complica una funcionalidad concreta sino todas a la vez.
La segunda pieza más subestimada es la coordinación entre pestañas, y lo es porque en las demostraciones nunca hay dos abiertas. Un usuario real abre tres, deja una en segundo plano una semana, restaura la sesión del navegador y espera que todo siga coherente. Eso obliga a elegir un escritor, a propagar cambios entre contextos, a decidir qué ocurre cuando el líder desaparece a mitad de una transacción y a probar todo ello en navegadores cuyo soporte de las primitivas necesarias no es uniforme. Es trabajo poco vistoso, difícil de estimar y absolutamente obligatorio, porque su ausencia se manifiesta como corrupción del almacén local, que es el peor fallo posible en esta arquitectura.
flowchart TB A[una funcionalidad nueva] --> B[modelo de datos] B --> C[como fusiona si dos dispositivos la editan a la vez] B --> D[como migra en dispositivos que no se actualizan] B --> E[que ocurre si el almacen local desaparece] B --> F[que ve el usuario mientras esta sin confirmar] C --> G[coste permanente por funcionalidad] D --> G E --> G F --> G style G fill:#f38ba8,color:#11111b
Ese diagrama es el verdadero contenido de la lección. En una arquitectura convencional, añadir un campo es añadir un campo. Aquí, añadir un campo obliga a responder cuatro preguntas de diseño, y ninguna tiene respuesta por defecto: hay que decidir si el campo es de último escritor, de conjunto, de contador o de texto colaborativo, hay que decidir qué valor toma para los clientes antiguos, hay que decidir si sobrevive a una reconstrucción y hay que decidir cómo se muestra mientras está sin confirmar. Multiplica esas cuatro decisiones por el número de campos que añadirás en tres años y tendrás el tamaño real del compromiso.
Hay un matiz que suaviza esa cuenta y conviene reconocerlo para no exagerar el argumento. Una parte de esas decisiones se estandariza: en cuanto el equipo fija convenciones —qué tipo de fusión usa cada clase de campo, cómo se nombran las versiones de esquema, qué se muestra mientras algo está sin confirmar— el coste marginal de cada campo nuevo baja bastante. El problema es que esas convenciones no vienen con la biblioteca, se descubren peleándose con casos reales, y hasta que existen cada decisión se toma desde cero y con criterio irregular. Ese periodo de aprendizaje es real, dura meses y en él se escriben las partes del sistema que más adelante habrá que reescribir.
En una arquitectura centralizada, la verdad está en tu servidor y puedes mirarla cuando quieras: consultas la base, lees los registros, reproduces el estado. En una replicada, la verdad relevante está en el dispositivo de un usuario al que no tienes acceso, y el estado que ves en tu servidor es solo lo que ese dispositivo tuvo a bien enviarte. Recuperar visibilidad exige construir herramientas propias: exportación del estado local, envío del registro de operaciones bajo consentimiento y un visor que reconstruya la historia causal. Es trabajo de producto, no de operaciones, y hay que planificarlo desde el principio porque después se necesita con urgencia.
La superficie de pruebas: el espacio de estados se multiplica
El coste de pruebas no crece de forma lineal porque las dimensiones se combinan. El estado observable de una funcionalidad ya no es su estado local, sino el producto cartesiano del estado local por el del servidor, por la conectividad, por la versión de esquema de cada extremo, por el número de pestañas abiertas, por el número de dispositivos y por el desfase entre relojes. Cada factor multiplica, y como se multiplican, la intuición sobre cobertura deja de funcionar muy pronto: un conjunto de casos que parecía exhaustivo cubre en realidad una fracción minúscula de las combinaciones posibles.
// Lo que hay que ejercitar para una sola funcionalidad, en combinacion.
const dimensiones = {
conectividad: ['con red', 'sin red', 'red intermitente', 'latencia alta'],
replicas: [1, 2, 5],
esquema: ['version actual', 'version anterior', 'mezcla'],
pestanas: [1, 3],
reloj: ['sincronizado', 'adelantado', 'atrasado'],
};
// 4 * 3 * 3 * 2 * 3 = 216 combinaciones por funcionalidad.
// Y ninguna de ellas es un test unitario: todas necesitan orquestacion.
La respuesta profesional a esa explosión no es escribir más casos de ejemplo, que es lo que hace la mayoría hasta que se rinde. Es cambiar de técnica: pruebas basadas en propiedades que generan secuencias aleatorias de operaciones y comprueban invariantes como la convergencia, y simulación determinista que ejecuta el sistema entero con un reloj y una red falsos bajo una semilla, de modo que un fallo encontrado se puede repetir exactamente. Estas técnicas son excelentes y también son una competencia que la mayoría de los equipos de producto no tiene, no contrata fácilmente y tarda meses en desarrollar. Ese es un coste real que rara vez aparece en la estimación.
Conviene además desactivar un consuelo frecuente: que la biblioteca elegida ya está probada y por tanto el problema está resuelto. Lo que está probado es la convergencia del tipo replicado, que es una propiedad matemática del algoritmo y que efectivamente puedes dar por buena. Lo que nadie ha probado es tu aplicación: que el orden en que tus operaciones se aplican preserva las reglas de tu dominio, que tu interfaz no muestra estados intermedios incoherentes, que tu migración funciona con dispositivos rezagados y que tu política de fusión hace lo que tu usuario espera. La corrección de la biblioteca es una condición necesaria y notablemente insuficiente, y confundirlas produce equipos que confían en garantías que nadie les dio.
Equipo de dos o tres personas
Sin holgura para construir simulación y herramientas de soporte, el coste de mantenimiento consume la capacidad de entregar producto. Es la señal más fiable de todas.
Sin nadie con experiencia distribuida
Alguien tiene que reconocer una anomalía de causalidad al verla. Aprenderlo sobre un producto en producción es la vía cara de adquirir ese conocimiento.
Producto en fase de descubrimiento
Si el modelo de datos aún cambia cada semana, cada cambio arrastra una migración distribuida. Estabiliza el dominio antes de replicarlo.
Lo instantáneo no diferencia
Si el usuario no elegiría tu producto por trabajar sin red, estás pagando la factura completa por una mejora que nadie va a notar en la decisión de compra.
Depurar lo que no puedes reproducir
Hay una clase de incidencia que solo existe en estos sistemas y que define su coste de soporte. Un usuario informa de que su documento perdió un párrafo. Ocurrió hace tres semanas, en un teléfono, durante un trayecto sin cobertura, con otra pestaña abierta en un portátil que se apagó a mitad. El estado que tienes delante es el resultado de la fusión, no su historia, y la historia es lo único que respondería la pregunta. Sin registro de operaciones conservado y exportable, la investigación es imposible, y lo peor no es no arreglarlo: es no poder ni confirmar que ocurrió.
De ahí se deriva la decisión de diseño más rentable de toda esta lección y también la más fácil de posponer. Conservar el registro causal de operaciones, con identificadores de dispositivo y marcas lógicas, y construir desde el primer día un mecanismo por el que un usuario pueda exportar ese registro y adjuntarlo a una incidencia. Cuesta poco al principio y es virtualmente imposible de añadir después, porque para entonces los datos que habrían explicado los fallos antiguos ya no existen. Conviene decidir también, y por escrito, cuánta historia se conserva: mucha ocupa espacio y filtra metadatos, poca deja el soporte a ciegas.
Hay un segundo efecto sobre el proceso del equipo que suele descubrirse tarde. En una aplicación convencional, un fallo grave se corrige y se despliega, y a los pocos minutos ya no existe para nadie. En un sistema replicado, un fallo que corrompió datos ya se ha propagado a otros dispositivos y a otras réplicas, y corregir el código no repara lo propagado: hace falta además una operación de reparación de datos que hay que escribir, probar y aplicar sobre estados que varían de un usuario a otro. Esa asimetría cambia el cálculo de riesgo de cada despliegue y, si el equipo no lo interioriza, la primera vez que ocurre pilla a todo el mundo sin plan.
El tiempo de depuración, por último, no se distribuye como el de una aplicación normal. La mayoría de los fallos son rápidos y aburridos, y luego hay una minoría que consume semanas: los que involucran orden causal, relojes o migraciones a medias. Esa distribución de cola larga es lo que rompe las planificaciones, porque la media es engañosa y la varianza es enorme. Planificar con la media es la forma habitual de llegar tarde a todo durante un trimestre entero sin entender por qué.
La diferencia entre un equipo que sobrevive a esta arquitectura y uno que se ahoga cabe en una pregunta: cuando llega un informe raro, cuánto tarda en tener el fallo delante otra vez. Si la respuesta es horas, el sistema es mantenible. Si es no lo conseguimos, cada incidencia se convierte en una conjetura y las conjeturas se acumulan. Construir esa capacidad —semillas deterministas, red simulada, reloj controlado, reproducción desde un registro exportado— es una inversión que no entrega ninguna funcionalidad visible y que decide si el producto es sostenible a los dos años.
Queda un coste del que casi nunca se habla porque no es técnico: el de coordinación con el resto de la organización. Soporte necesita formación distinta, porque las incidencias tienen otra forma y las respuestas de siempre no sirven. Producto necesita entender por qué una funcionalidad trivial en otra arquitectura aquí requiere una decisión de fusión. Seguridad y cumplimiento necesitan revisar decisiones que antes ni existían. Ese trabajo de traducción recae normalmente sobre una o dos personas del equipo técnico, no aparece en ninguna estimación y consume una cantidad de tiempo sorprendente durante el primer año.
Un fallo de sincronización no se percibe como un error de software normal. Se percibe como que la aplicación perdió trabajo, y esa percepción es cualitativamente distinta: un usuario perdona una pantalla lenta muchas veces y no perdona una pérdida de datos ninguna. La consecuencia práctica es que el listón de calidad de un sistema replicado no lo fija tu equipo sino la naturaleza del fallo, y está mucho más alto que el de una aplicación convencional. Con un incidente basta para que alguien empiece a copiar su trabajo en otro sitio por si acaso, y ese usuario ya no vuelve del todo.
Ese listón más alto tiene una consecuencia concreta sobre el diseño que conviene extraer: las funciones de recuperación dejan de ser opcionales. Historial de versiones accesible por el usuario, papelera con retención generosa, exportación completa en un formato que se pueda abrir sin tu aplicación. En un sistema convencional son mejoras agradables; aquí son la red de seguridad que convierte un fallo de fusión en una molestia recuperable en lugar de una pérdida definitiva. Presupuestarlas desde el principio no es prudencia excesiva: es reconocer que ninguna implementación va a ser perfecta y decidir por adelantado qué pasa cuando no lo sea.
El cálculo para un equipo pequeño
El razonamiento honesto para un equipo de pocas personas empieza reconociendo que parte del coste se paga de todos modos. Si ya tienes actualizaciones optimistas, una caché con invalidación, una cola de peticiones fallidas y reintentos, ya estás construyendo un motor de sincronización mediocre sin llamarlo así, y probablemente con peores garantías que uno bueno. Adoptar la disciplina desde el principio, cuando el modelo de datos aún es maleable, es sensiblemente más barato que reconvertir una aplicación madura, donde cada tabla existente hay que reinterpretarla como estructura fusionable.
Conviene sin embargo no exagerar ese consuelo, porque tiene un límite claro. Que ya estés pagando parte de la factura no significa que estés pagando la parte cara. Una caché optimista con reintentos resuelve el caso de un usuario y un dispositivo con red intermitente; el motor de sincronización resuelve el caso de varios usuarios y varios dispositivos con historias divergentes. La distancia entre esos dos problemas es la mayor parte del coste, y es justo la que el argumento de ya lo estás haciendo de todos modos deja fuera.
Reconocido eso, el resto del cálculo apunta en la otra dirección. El compromiso incluye una dependencia sobre un ecosistema joven, con pocas personas contratables que lo dominen y con riesgo real de abandono del proyecto; y salir de un motor de sincronización es mucho más difícil que cambiar de biblioteca de acceso a datos, porque el formato en el que se guarda la información es el producto, no un detalle interno. Añade que el impuesto por funcionalidad no desaparece nunca: cada campo nuevo, durante toda la vida del sistema, obliga a decidir cómo fusiona y cómo migra. Un equipo pequeño no compra una funcionalidad, compra una obligación recurrente.
La conclusión operativa cabe en una regla. Si trabajar sin red o de forma instantánea es la razón por la que alguien elegiría tu producto frente al de al lado, el coste está justificado y hay que asumirlo entero, con simulación, instrumentación y herramientas de soporte incluidas. Si es una mejora agradable que quedaría bien en la página de características, hay opciones que capturan la mayor parte del beneficio percibido por una fracción del coste, y elegirlas no es cobardía técnica sino asignación correcta de un presupuesto de complejidad que es finito.
Ese presupuesto merece tomarse en serio como concepto, porque es lo que convierte la discusión en una decisión y no en una preferencia. Todo equipo tiene una cantidad limitada de complejidad que puede sostener a la vez, y gastarla es una elección excluyente: la que dedicas a la sincronización no está disponible para el motor de reglas, la internacionalización, la accesibilidad o el rendimiento de la interfaz. Un equipo que decide gastar la mayor parte de su presupuesto en el enfoque local-first está haciendo una apuesta legítima y muy concreta sobre dónde está su ventaja competitiva. El problema no es hacer esa apuesta, es hacerla sin darse cuenta de que la ha hecho, y descubrirlo cuando el resto del producto lleva un año sin avanzar.
Conviene terminar señalando lo que este argumento no dice, porque el escepticismo mal calibrado es tan inútil como el entusiasmo. No dice que la arquitectura sea mala, ni que su coste sea injustificable, ni que los equipos que la adoptan se equivoquen. Dice que el coste existe, que es mayor de lo que se anuncia, que se paga durante años y que hay que decidirlo con los números delante en lugar de con el entusiasmo de una conferencia. Un equipo que hace ese cálculo y decide seguir adelante está en una posición mucho más fuerte que uno que lo evita, entre otras cosas porque sabrá reconocer cuándo la apuesta está saliendo mal y podrá corregir a tiempo.
El marco que ordena toda esta lección es que la complejidad de un sistema distribuido es una cantidad que se conserva y que lo único que se decide es dónde reside y quién la paga. Una arquitectura centralizada concentra la dificultad en un punto, la resuelve con transacciones y bloqueos que llevan cincuenta años depurándose, y pasa la factura al usuario en forma de latencia y de dependencia de la red: es un coste continuo, visible, molesto y perfectamente conocido. Una arquitectura replicada le devuelve al usuario esa latencia y traslada la dificultad al equipo de ingeniería en forma de semántica de fusión, migraciones distribuidas, causalidad y fallos irreproducibles: es un coste diferido, invisible en la demostración, y que se cobra durante años en cada funcionalidad futura. Ninguna de las dos opciones es intrínsecamente superior, y quien afirme lo contrario está describiendo su contexto y no una propiedad de la ingeniería. La decisión madura consiste en preguntarse quién puede pagar mejor esa factura en tu situación concreta: si tu producto vive de que el trabajo sea inmediato y tu equipo tiene músculo para sostener la disciplina, trasladar el coste al equipo es una inversión excelente; si tu ventaja está en otra parte y tu equipo son tres personas con un plazo, dejar la complejidad donde la industria ya la resolvió es la decisión profesional, aunque sea la menos vistosa de contar en una charla. Elegir arquitectura es elegir qué problema quieres tener, no elegir no tener problemas.
- Toma una funcionalidad reciente de tu producto y responde por escrito las cuatro preguntas del diagrama. Cronometra cuánto tardas.
- Multiplica ese tiempo por el número de funcionalidades previstas para el próximo año. Compara la cifra con el ahorro de latencia que esperas obtener.
- Enumera las piezas nuevas que tendrías que construir y mantener. Marca cuáles no tiene experiencia previa nadie del equipo.
- Diseña el mecanismo de exportación del registro causal para soporte. Si no sabes qué incluiría, aún no sabes depurar el sistema que quieres construir.
- Simula una incidencia: dado un informe vago de pérdida de datos con tres semanas de antigüedad, describe paso a paso cómo la investigarías con lo que hoy tienes instrumentado.
- Escribe el plan de salida: si dentro de dos años quisieras abandonar el motor elegido, describe cómo migrarías los datos y cuánto costaría.