wandres.dev
LOS SIETE IDEALES · qué promete de verdad

Ideales de rendimiento y red: sin esperas, multidispositivo, sin conexión

Los tres primeros ideales exigen que el camino crítico de lectura y escritura no cruce la red, que cada dispositivo tenga una réplica completa y que la desconexión sea el supuesto por defecto y no un error.

⏱ 16 min

Los tres primeros ideales suenan a lista de deseos de producto: que sea rápido, que funcione en todos mis aparatos y que aguante sin cobertura. No lo son. Cada uno impone una restricción estructural verificable sobre el camino de escritura, sobre el modelo de identidad y sobre el manejo de errores. Este es el bloque donde local-first deja de ser una postura y se convierte en ingeniería.

🎯 Al terminar esta lección sabrás
  • Traducir “sin ruedas de carga” a una restricción sobre el camino crítico.
  • Separar la interfaz optimista de la escritura autoritativa local.
  • Entender qué exige el multidispositivo del modelo de identidad y de identificadores.
  • Ver la desconexión como partición permanente y no como excepción.

Ideal 1: sin ruedas de carga

La formulación cruda es esta: ninguna interacción del usuario puede depender de una ida y vuelta por la red para completarse. No “casi ninguna”, no “salvo la primera”. Ninguna.

La razón no es estética. La percepción humana tiene umbrales bien medidos: por debajo de unos 100 milisegundos una respuesta se siente instantánea y el usuario mantiene la sensación de manipular directamente el objeto; a partir de unos cientos de milisegundos la atención se fragmenta y el usuario empieza a esperar en lugar de pensar. Una petición a un centro de datos regional cuesta, en el mejor caso, decenas de milisegundos de ida y vuelta; con radio móvil, congestión o pérdida de paquetes, cientos o miles. El presupuesto de latencia se agota en el transporte antes de que empiece el trabajo útil.

La consecuencia arquitectónica es que el estado que la interfaz pinta debe resolverse en el proceso, contra una estructura en memoria respaldada por almacenamiento local. La red se mueve fuera del camino crítico, a una tarea de fondo que no bloquea nada.

Compara los dos caminos de escritura. El primero es el habitual en una aplicación de nube.

// Camino orientado a servidor: la interfaz espera una respuesta remota.
async function renombrar(id, nombre) {
  const res = await peticion('PATCH', '/doc/' + id, { nombre });
  if (!res.ok) throw new Error('fallo de red o de validacion');
  pintar(res.cuerpo);
}

El segundo es el camino local-first.

// Camino local-first: la interfaz depende solo del estado local.
function renombrar(id, nombre) {
  const op = { tipo: 'renombrar', id, nombre, reloj: reloj.tick() };
  estado.aplicar(op);          // sincrono, en memoria
  registro.anadir(op);         // durable, en disco local
  pintar(estado);              // la interfaz ya puede pintar
  sincronizador.encolar(op);   // la red ocurre despues, si es que ocurre
}

La diferencia no está en la sintaxis ni en la ausencia de await. Está en quién decide si la operación es válida. En el primer caso, el servidor; la interfaz solo refleja un veredicto ajeno. En el segundo, la réplica local; la sincronización posterior no puede rechazar la operación, solo integrarla con las demás.

⚠️
Interfaz optimista no es local-first

La interfaz optimista pinta el resultado antes de confirmarlo y deshace si el servidor rechaza. Es una mentira retractable: mejora la latencia percibida sin cambiar dónde reside la autoridad. Su modo de fallo es la reversión sorpresa, que aparece cuando peor viene: con mala cobertura y trabajo acumulado. Local-first elimina la reversión porque no hay veredicto remoto que esperar; el precio es que la reconciliación debe ser convergente por construcción. Si tu diseño necesita poder rechazar una escritura ya aplicada localmente, no has cruzado la frontera.

flowchart LR
U[Interaccion del usuario] --> E[Aplicar a la replica local]
E --> D[Escribir en el registro durable]
D --> P[Pintar la interfaz]
E --> Q[Cola de sincronizacion fuera del camino critico]
Q --> R[Red oportunista]
R --> O[Otras replicas]
style E fill:#a6e3a1,color:#11111b
style P fill:#a6e3a1,color:#11111b
style R fill:#f9e2af,color:#11111b

Ideal 2: tu trabajo no vive en un solo dispositivo

El segundo ideal parece el más fácil y es el que rompe más suposiciones heredadas.

Exige que cada dispositivo sostenga una réplica completa y autoritativa, no una vista parcial servida bajo demanda. Y exige que la convergencia entre esas réplicas sea automática y simétrica: ninguna de ellas es la principal, ninguna espera turno.

Tres suposiciones caen de golpe.

La primera es el identificador asignado por el servidor. Una clave autoincremental exige un punto de serialización global antes de que exista la entidad. Si un dispositivo desconectado crea un elemento, no puede pedir permiso a nadie para nombrarlo. Los identificadores deben ser generables localmente y colisionar con probabilidad despreciable: identificadores aleatorios de suficiente entropía, o pares de contador lógico e identificador de réplica.

La segunda es la sesión como sede de la identidad. Si “quién eres” es una fila en la tabla de sesiones de un servidor, entonces cerrar sesión o quedarte sin red te separa de tu trabajo. En una arquitectura de réplicas, la identidad debe ser algo que el dispositivo posee: material criptográfico, no un permiso concedido remotamente.

La tercera es la marca de tiempo del reloj de pared como criterio de orden. Los relojes de dos dispositivos discrepan, y la discrepancia no está acotada. El orden entre eventos concurrentes debe derivarse de causalidad registrada, no de la hora que marcaba cada máquina.

// Identidad de una operacion generable sin coordinacion.
type IdOperacion = {
  replica: string;   // identificador estable del dispositivo
  contador: number;  // monotono dentro de esa replica
};

// Orden total determinista: causalidad primero, desempate estable despues.
function comparar(a: IdOperacion, b: IdOperacion): number {
  if (a.contador !== b.contador) return a.contador - b.contador;
  return a.replica < b.replica ? -1 : 1;
}

El desempate por identificador de réplica no es arbitrario: es lo que garantiza que dos dispositivos que han visto el mismo conjunto de operaciones lleguen al mismo estado sin hablar entre sí. La convergencia se apoya en el determinismo, no en un árbitro.

📝
Sincronizar ficheros no es sincronizar estado

Una carpeta sincronizada resuelve el ideal 2 a nivel de bytes y falla a nivel de significado: cuando dos dispositivos modifican el mismo fichero, el sistema produce dos copias con nombres distintos y delega el problema en la persona. Es una solución honesta y muy longeva, pero no satisface el ideal 4. La distinción importa: replicar bytes es fácil, replicar intención editorial es el problema difícil.

Ideal 3: la red es opcional

El tercer ideal invierte una asimetría profunda del software de red. En la arquitectura convencional, la conexión es el caso normal y su ausencia es una excepción que se atrapa, se registra y se muestra como error. En local-first, la ausencia de conexión es el caso base, y la conexión es una optimización oportunista que puede ocurrir o no.

En términos del teorema de particiones, la elección está tomada de antemano: ante una partición, el sistema conserva disponibilidad y renuncia a la consistencia fuerte, apostando por la convergencia posterior. Pero conviene precisar algo que el vocabulario clásico oscurece: aquí la partición no es un accidente transitorio de milisegundos, es una condición planificada y potencialmente indefinida. Un dispositivo puede estar semanas sin ver a los demás y debe seguir siendo plenamente utilizable.

Eso impone tres requisitos que se olvidan con facilidad.

Durabilidad antes que transmisión. Una operación debe estar en almacenamiento persistente local antes de considerarse aceptada. Si vive solo en memoria a la espera de enviarse, un cierre inesperado la destruye y el sistema ha mentido al usuario.

Metadatos causales, no solo carga útil. Para reconciliar tras semanas de divergencia hace falta saber qué había visto cada réplica cuando produjo cada operación. Sin ese contexto, la fusión degenera en “gana el último que llegó”, que es una pérdida de datos silenciosa disfrazada de resolución.

Arranque en frío sin red. La prueba decisiva no es seguir trabajando tras perder cobertura con la aplicación ya abierta. Es cerrarla, reiniciar el dispositivo en modo avión, abrirla y poder escribir. Ahí se descubren las dependencias ocultas: validación de licencia, refresco de credenciales, descarga de configuración, comprobación de versión mínima.

Hay además una consecuencia sobre el diseño de la interfaz que suele pasarse por alto. Si la desconexión es el caso base, entonces el indicador de estado de red deja de ser una alarma y pasa a ser información secundaria, como el nivel de batería. Un producto que muestra un aviso rojo al perder la conexión está confesando, en su propia interfaz, que su autoridad es remota: si de verdad no necesitara la red para operar, perderla no sería un evento digno de interrumpir a nadie. La reconexión, simétricamente, no debe presentarse como una recuperación ni exigir confirmación; es simplemente el momento en que llega trabajo ajeno.

💡
La prueba del avión frío

Modo avión, cierre completo del proceso, reinicio del dispositivo, apertura. Si aparece cualquier pantalla que no sea tu documento, tienes una dependencia de red en el arranque. Es la prueba más barata del marco y la que más productos suspende.

Ideal 1

Restringe el camino crítico: leer y escribir sin cruzar la frontera del proceso.

📱

Ideal 2

Restringe la identidad y los identificadores: generables sin coordinación previa.

✈️

Ideal 3

Restringe la durabilidad y el arranque: nada esencial puede esperar a la red.

Puestas juntas, las tres restricciones describen un único programa: uno que arranca leyendo de disco, mantiene todo su estado de trabajo en memoria, acepta cada operación de forma síncrona y determinista, la registra de forma durable antes de confirmarla, y delega en un componente completamente desacoplado la tarea eventual de intercambiar operaciones con otras réplicas. Ese componente puede caer, tardar horas, reiniciarse o no existir, y la experiencia de uso no cambia. Es, deliberadamente, la misma forma que tenía una aplicación de escritorio de los años noventa, con una única diferencia crucial: el fichero ya no es un bloque opaco que se sobrescribe entero, sino una secuencia de operaciones con identidad y contexto causal que puede fusionarse con la de otro. Esa es exactamente la pieza que los ideales 4 y 5 desarrollan, y también donde empiezan los compromisos.

Los tres son el mismo teorema

Es tentador tratar estos tres ideales como preocupaciones separadas —rendimiento, portabilidad y tolerancia a fallos— y asignarlos a equipos distintos. Son la misma restricción vista desde tres ángulos: el estado autoritativo debe residir donde ocurre la interacción. El ideal 1 es esa restricción medida en el eje del tiempo, el ideal 2 en el eje del espacio, y el ideal 3 en el eje de la disponibilidad. Por eso no se pueden conseguir por separado ni añadir después: un sistema que resuelve el 1 con caché optimista sigue suspendiendo el 3, porque su autoridad sigue siendo remota; uno que resuelve el 3 con una cola de reintentos sigue suspendiendo el 2, porque su reconciliación no converge; y uno que resuelve el 2 con un servidor central sigue suspendiendo el 1, porque su camino crítico cruza la red. La consecuencia práctica es incómoda pero liberadora: no existe una migración incremental hacia estos tres ideales. O mueves la autoridad al dispositivo y aceptas que la fusión sea tu problema difícil, o te quedas donde estás y describes tu producto con honestidad. Los atajos intermedios producen sistemas que fallan en los tres ejes a la vez, y que además fallan de forma impredecible.

⚔️ Audita el camino crítico
  1. Traza el camino de escritura de una aplicación que uses y marca en qué punto exacto interviene la red.
  2. Explica por qué una clave autoincremental asignada por el servidor es incompatible con el ideal 2.
  3. Argumenta por qué el reloj de pared no sirve para ordenar eventos concurrentes entre dispositivos.
  4. Ejecuta la prueba del avión frío sobre tres aplicaciones y clasifica lo que falla en cada una.
  5. Distingue, con un ejemplo propio, entre interfaz optimista con reversión y escritura autoritativa local.