Saltar al contenido
Arquitectura de software

Offline-first: cómo diseñar apps que funcionan sin internet

Qué es la arquitectura offline-first, cómo funcionan la base local, la cola de escritura y la sincronización, y cuándo conviene usarla.

Equipo menululo · Equipo fundador de menululo, Cali

Publicado el · 7 min de lectura

En este artículo respondemos:

¿Qué es una aplicación offline-first y cómo funciona?

Resumen

  • Offline-first es diseñar la app para que lea y escriba primero en el dispositivo y trate la red como algo que llega y se va.
  • Sus piezas básicas son cuatro: base de datos local, cola de escritura, motor de sincronización y resolución de conflictos.
  • No sirve para todo: cuando una operación exige confirmación inmediata del servidor, el diseño tiene que decirlo explícitamente.

¿Qué significa offline-first?

Offline-first es un enfoque de arquitectura en el que la aplicación funciona con los datos guardados en el dispositivo y usa la red solo para sincronizar. La falta de conexión se trata como un estado normal, no como un error.

La mayoría de las apps tradicionales siguen el camino contrario: cada pantalla pide datos al servidor, espera la respuesta y recién entonces muestra algo. Si la red falla, aparece un indicador de carga eterno o un mensaje de error. En una app offline-first, la interfaz lee de una base local que responde en milisegundos, y un proceso aparte se encarga de que esa base y el servidor terminen diciendo lo mismo.

Este cambio de orden tiene un efecto que va más allá de los túneles y los sótanos sin señal: la app se siente rápida siempre, porque ninguna interacción espera un viaje de ida y vuelta por la red.

¿En qué se diferencia de un simple “modo sin conexión”?

Un modo sin conexión suele ser un parche que guarda en caché lo último que se vio; offline-first es una decisión que se toma en el modelo de datos desde el primer día.

La diferencia se nota al escribir. Una caché permite leer sin red, pero cuando el usuario crea o edita algo, la app no sabe qué hacer. En un diseño offline-first, cada escritura tiene un camino definido: se guarda localmente, se encola, se envía cuando se pueda y se reconcilia si alguien más cambió lo mismo.

¿Cuáles son las piezas de una arquitectura offline-first?

Son cuatro componentes que trabajan juntos: la base local, la cola de escritura, el motor de sincronización y la estrategia de conflictos.

1. Base de datos local

Es la fuente de datos de la interfaz. Puede ser una base embebida en el dispositivo o, en la web, IndexedDB. La regla de oro: la pantalla nunca lee directamente del servidor, siempre lee de la base local y reacciona cuando esta cambia.

2. Cola de escritura (outbox)

Cada cambio que hace el usuario se guarda en la base local y, en la misma transacción, se registra en una tabla de operaciones pendientes. Si se guardan por separado, un cierre inesperado de la app entre ambos pasos deja un dato que el servidor nunca va a recibir. Es la misma idea que el patrón de bandeja de salida transaccional que se usa en sistemas de backend.

// Guardar el cambio y encolarlo en UNA sola transacción local
async function guardarNota(db: LocalDb, nota: Nota) {
  await db.transaction(async (tx) => {
    await tx.put("notas", nota);
    await tx.put("outbox", {
      id: crypto.randomUUID(),   // sirve también como clave de idempotencia
      tipo: "nota.upsert",
      payload: nota,
      creadoEn: Date.now(),
      intentos: 0,
    });
  });
}

3. Motor de sincronización

Es un proceso en segundo plano con dos direcciones:

  • Subida (push): toma las operaciones de la cola, las envía al servidor y las borra solo cuando el servidor confirma.
  • Bajada (pull): pide los cambios ocurridos desde la última sincronización (normalmente con un cursor o una marca de versión) y los aplica en la base local.

Como la subida puede fallar a mitad de camino y reintentarse, cada operación tiene que ser idempotente: aplicarla dos veces debe dejar el mismo resultado que aplicarla una. Profundizamos en esto en colas de reintentos, idempotencia y backoff.

4. Resolución de conflictos

Si dos dispositivos modifican el mismo registro mientras están desconectados, al reconectar hay dos versiones. Algo tiene que decidir cuál queda o cómo se combinan.

¿Cómo se sincroniza cuando vuelve la conexión?

El motor detecta la reconexión, vacía la cola en orden y luego descarga lo que cambió en el servidor. El orden importa: subir primero evita que un cambio remoto pise uno local que todavía no se envió.

Un bucle simplificado se ve así:

async function sincronizar(db: LocalDb, api: Api) {
  // 1. Subir pendientes, en orden de creación
  for (const op of await db.getAll("outbox", { orderBy: "creadoEn" })) {
    try {
      await api.aplicar(op, { idempotencyKey: op.id });
      await db.delete("outbox", op.id);
    } catch (e) {
      if (esErrorTransitorio(e)) break;        // reintentar más tarde con backoff
      await db.moverA("outbox_fallidas", op);   // error permanente: no bloquear la cola
    }
  }
  // 2. Bajar cambios desde el último cursor
  const cursor = await db.getMeta("cursor");
  const { cambios, nuevoCursor } = await api.cambiosDesde(cursor);
  await db.transaction(async (tx) => {
    for (const c of cambios) await aplicarConResolucion(tx, c);
    await tx.setMeta("cursor", nuevoCursor);
  });
}

Dos detalles que suelen olvidarse: el cursor se actualiza en la misma transacción que los cambios aplicados, y una operación que falla de forma permanente se aparta para revisión en lugar de bloquear todas las que vienen detrás.

¿Qué pasa cuando dos dispositivos editan lo mismo?

Hay un conflicto, y la estrategia para resolverlo depende del tipo de dato. No existe una solución universal.

Las opciones más usadas son:

  • Last-write-wins (LWW): gana la última escritura. Es simple, pero descarta cambios en silencio, y “la última” depende de relojes que pueden estar desfasados.
  • Fusión por campo: si un dispositivo cambió el título y otro la descripción, se conservan ambos cambios. Solo hay conflicto real si tocaron el mismo campo.
  • Tipos de datos que convergen solos (CRDT): estructuras diseñadas matemáticamente para que todas las réplicas lleguen al mismo estado sin coordinación, como describen Shapiro y colegas (2011).
  • Preguntar al usuario: útil cuando el dato es valioso y la decisión no se puede automatizar.

Cada una tiene costos distintos. Los explicamos con ejemplos en algoritmos de sincronización: LWW, relojes vectoriales y CRDT.

¿Cómo se hace offline-first en la web?

Con tres herramientas del navegador: service workers, IndexedDB y, donde esté disponible, Background Sync.

  • Un service worker es un script que corre en un hilo aparte y puede interceptar peticiones de red. Permite servir la app aunque no haya conexión, como explica la guía de MDN sobre operación offline y en segundo plano.
  • IndexedDB es la base de datos transaccional del navegador, adecuada para la base local y la cola de escritura.
  • La API de Background Synchronization permite diferir tareas hasta que haya una conexión estable. Según MDN, no forma parte de Baseline porque no funciona en algunos de los navegadores más usados, así que conviene tener un respaldo que escuche el evento online y sincronice al abrir la app.

¿Qué es local-first y en qué se parece?

Local-first es una idea emparentada pero más ambiciosa: además de funcionar sin conexión, busca que los datos sigan siendo del usuario aunque el servicio desaparezca.

El término viene del ensayo Local-first software publicado en 2019 por Martin Kleppmann, Adam Wiggins, Peter van Hardenberg y Mark McGranaghan, del laboratorio Ink & Switch. Proponen siete ideales: rapidez, múltiples dispositivos, trabajo sin conexión, colaboración, longevidad, privacidad y control del usuario. Offline-first cubre sobre todo los tres primeros; local-first va por todos.

¿Cuándo no conviene offline-first?

Cuando la operación solo es válida si el servidor la confirma en ese instante. Ejemplos genéricos:

  • Reservar el último asiento de un vuelo o la última unidad de un inventario compartido.
  • Transferir dinero, donde el saldo debe verificarse contra la fuente central.
  • Cambiar permisos de acceso, donde un dispositivo desconectado no debería seguir actuando con permisos revocados.

En esos casos se suele usar un diseño mixto: la mayor parte de la app funciona sin conexión y las operaciones críticas se marcan como “requiere conexión”, con un mensaje claro para el usuario. También hay que considerar el costo: sincronización, migraciones del esquema local y pruebas de conflictos agregan complejidad real.

¿Qué errores son los más comunes?

Casi todos vienen de tratar la sincronización como un detalle y no como parte del modelo de datos.

  1. Guardar y encolar por separado, lo que pierde cambios si la app se cierra en el medio.
  2. Operaciones no idempotentes, que duplican registros cuando un envío se reintenta.
  3. Usar la hora del dispositivo como verdad, cuando los relojes de los dispositivos pueden estar desfasados.
  4. Borrar registros de verdad en lugar de marcarlos como eliminados, lo que hace que otro dispositivo los “resucite” al sincronizar.
  5. Olvidar las migraciones locales: un usuario puede abrir una versión vieja de la app semanas después, con datos en un esquema antiguo y operaciones pendientes en la cola.
  6. Probar solo con buena conexión. Las fallas aparecen con redes lentas, cortes a mitad de envío y dos dispositivos editando a la vez.

Para cerrar

Offline-first no es una función que se agrega al final: es invertir el orden de la arquitectura para que el dispositivo sea el punto de partida y la red, un canal de sincronización. Bien hecho, da apps rápidas y resistentes; mal hecho, da datos duplicados y cambios perdidos. Si vas a construir la parte de red, sigue con colas de reintentos; si te preocupan los conflictos, con algoritmos de sincronización.

Preguntas frecuentes

¿Offline-first significa que la app nunca usa internet?

No. Una app offline-first sí usa la red, pero no depende de ella para funcionar. Lee y escribe primero en una base de datos local y sincroniza con el servidor cuando hay conexión, de modo que la falta de señal retrasa la sincronización sin bloquear al usuario.

¿Qué pasa si dos personas editan el mismo dato sin conexión?

Se produce un conflicto que el motor de sincronización debe resolver al reconectar. Las estrategias más comunes son last-write-wins, fusión por campo, estructuras CRDT que convergen solas o pedirle al usuario que elija. La estrategia se decide al diseñar el modelo de datos, no después.

¿Se puede construir una app web offline-first?

Sí. La web ofrece service workers para servir la app sin red e IndexedDB para guardar datos en el navegador. La API de Background Sync ayuda a reenviar cambios pendientes, pero no funciona en todos los navegadores principales, así que conviene tener un respaldo con el evento online.

Sigue leyendo

Más artículos técnicos