🏗️ Arquitectura de Sincronización Offline-First entre App Móvil y Backend
🎯 Objetivo
Diseñar un proceso completo y robusto para la sincronización de datos (catálogos y tablas transaccionales) entre una aplicación móvil y un sistema backend con soporte Offline-First.
🧩 Componentes de la Arquitectura
📱 App Móvil
- DB Local: (SQLite/Realm) con catálogos, transacciones y tablas de control (
pending_outbox,sync_state,oplog). - Outbox: registra todas las operaciones CRUD realizadas sin conexión.
- Motor de Sync: detecta conectividad, maneja colas y aplica deltas desde el backend.
- Conflict Resolver: gestiona conflictos (LWW o cliente-gana).
- Manejador de Push: escucha notificaciones silenciosas y dispara sincronizaciones automáticas.
🌐 API Backend
- Endpoints de sincronización (batch e idempotentes).
- Módulo de catálogos con deltas y versionado.
- Resolución de conflictos y validación de negocio.
- Control de dispositivos (tokens, versiones, sincronización remota).
☁️ Servicio de Push
- Usa FCM (Android) o APNs (iOS).
- Envía notificaciones silenciosas (
content-available=1) para iniciar sincronizaciones sin alertar al usuario.
🗄️ Base de Datos Servidor
- Tablas maestras y operacionales con campos
updated_at,version, oetag. - Control de versiones para catálogos y journaling para generar deltas.
🖥️ Servicio de Gestión de Dispositivos
- Almacena
device_id,push_token,app_version,last_seen,platform. - Permite sincronizaciones dirigidas a segmentos de usuarios o zonas específicas.
⚙️ Estrategias Técnicas
1️⃣ Detección de Cambios (Cliente → Servidor)
Tabla pending_outbox con:
id(UUID),op(CREATE/UPDATE/DELETE),entity,entity_id,payload,idempotency_key,status,retry_count,created_at_local.- La app siempre escribe localmente primero, luego registra en
pending_outbox. - El motor de sync procesa los registros
PENDINGen lotes.
2️⃣ Actualización de Catálogos (Servidor → Cliente)
- Modelo “delta by timestamp/version”:
- El cliente guarda
last_catalog_sync_atpor catálogo. - El backend expone
/catalogs/{name}/delta?since=timestamp. - Devuelve
upserts[]ytombstones[]conupdated_atoversion.
- El cliente guarda
3️⃣ Manejo de Lotes
- Tamaños configurables (50–200 items).
- Reintentos con backoff exponencial y jitter.
- Endpoint batch idempotente con
idempotency_keypersistido en backend (TTL 14 días).
4️⃣ Resolución de Conflictos
| Tipo de dato | Estrategia | Regla |
|---|---|---|
| Catálogos | LWW (Last Write Wins) | El servidor gana |
| Transacciones nuevas | Cliente gana | Se aceptan si no violan reglas de negocio |
| Updates sobre misma entidad | Optimistic Concurrency | If-Match: <etag> o version |
5️⃣ Atomicidad
- Transacción por item dentro del batch.
- Éxito parcial permitido, respuesta con matriz de resultados.
- Modo opcional
atomic=truepara lotes que deben fallar todos o ninguno.
6️⃣ Seguridad
- Autenticación OAuth2.0/JWT.
- Conexión segura TLS.
- Límite de payload (512KB–2MB) y compresión GZIP.
- Trazas con
idempotency_key,device_id,sync_session_id.
🧠 Diagrama de Flujo – Lógica del Cliente (Offline-First)
🔁 Diagrama de Secuencia – Sincronización de Transacciones
📡 Diagrama de Secuencia – Sincronización Forzada (Push Silenciosa)
🗃️ Esquema de Base de Datos Local
CREATE TABLE pending_outbox (
id TEXT PRIMARY KEY,
op TEXT NOT NULL,
entity TEXT NOT NULL,
entity_id TEXT NOT NULL,
payload TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
created_at_local TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING',
retry_count INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE sync_state (
key TEXT PRIMARY KEY,
last_sync_at TEXT,
last_version INTEGER
);
CREATE TABLE cat_products (
id TEXT PRIMARY KEY,
sku TEXT,
name TEXT,
price REAL,
updated_at TEXT,
version INTEGER
);
🧩 Endpoints REST Recomendados
Transacciones
POST /transactions/batch
Request:
{
"items": [
{
"idempotency_key": "uuid",
"op": "CREATE",
"entity": "order",
"client_id": "uuid",
"payload": { "field": "value" }
}
],
"device": { "id": "dev123", "app_version": "1.2.0" }
}
Response:
{
"results": [
{
"idempotency_key": "uuid",
"status": "OK",
"server_id": "srv-001",
"etag": "v2",
"message": "Success"
}
]
}
Catálogos
GET /catalogs/{name}/delta?since=2025-01-01T00:00:00Z
Response:
{
"upserts": [{ "id": "p1", "name": "Leche Alpura", "price": 22.5 }],
"tombstones": [{ "id": "p2", "deleted_at": "2025-10-01T00:00:00Z" }],
"server_time": "2025-10-22T10:00:00Z",
"version": 15
}
Catálogos
GET /catalogs/{name}/delta?since=2025-01-01T00:00:00Z
Response:
{
"upserts": [{ "id": "p1", "name": "Leche Alpura", "price": 22.5 }],
"tombstones": [{ "id": "p2", "deleted_at": "2025-10-01T00:00:00Z" }],
"server_time": "2025-10-22T10:00:00Z",
"version": 15
}
Sincronización
POST /sync/ack
Body:
{ "device_id": "dev123", "catalog": "products", "version": 15 }
🔄 Pseudocódigo de Ciclo de Sincronización
function syncNow():
if !isNetworkAvailable(): return
// 1) Transacciones -> Servidor
items = db.outbox.fetch(limit = adaptiveBatchSize())
if items.isEmpty(): goto catalogs
resp = api.post("/transactions/batch", {items})
for each r in resp.results:
if r.status == "OK":
db.outbox.markSent(r.idempotency_key)
else if r.status == "CONFLICT":
db.outbox.markFailed(r.idempotency_key)
else:
db.outbox.incrementRetry(r.idempotency_key)
// 2) Catálogos <- Servidor
catalogs:
for cat in enabledCatalogs():
since = db.sync_state.get(cat).last_sync_at
do:
delta = api.get("/catalogs/"+cat+"/delta?since="+since)
db.tx {
applyUpserts(cat, delta.upserts)
applyDeletes(cat, delta.tombstones)
db.sync_state.update(cat, delta.server_time)
}
while delta.next_cursor != null
📈 Políticas de Reintento
- Errores 5xx / red: backoff exponencial (2ⁿ segundos, máx 5 min).
- Errores 4xx (409, 412): marcar
FAILED, posible resolución manual. - Timeouts: reducir tamaño de lote dinámicamente.
✅ Decisiones de Diseño por Defecto
| Aspecto | Política |
|---|---|
| Conflictos Catálogos | Servidor gana (LWW) |
| Conflictos Transacciones | Cliente gana (validación posterior) |
| Atomicidad | Por item (no todo-o-nada) |
| Tamaño de lote | 50–200 items |
| Idempotencia (server) | TTL 14 días |
| Sync forzada | Silent push segmentada por device_tag |
📊 Métricas y Observabilidad
- Métricas: tamaño de lote, tasa de conflicto, duración de sync.
- Logs:
idempotency_key,device_id,sync_session_id. - Alertas: spikes de
CONFLICTo latencias elevadas.
🕒 Sincronización de Catálogos Programada (una vez al día)
Esta sección agrega un planificador diario para refrescar catálogos aunque no haya actividad del usuario, complementando la sincronización por eventos y por push silenciosa.
Objetivos
- Garantizar que los catálogos estén actualizados al menos 1 vez al día.
- Desacoplar del uso activo (si el usuario no abre la app).
- Respetar batería y datos: ventana con backoff, solo en red no medida si es posible, y tamaño de delta paginado.
Política sugerida
- Horario sugerido: ventana entre 02:00–04:00 hora local (configurable por feature flag).
- Estrategia: “delta by timestamp/version” por catálogo (products, clients, prices, routes, etc.).
- Stagger por dispositivo**:** aplicar un jitter aleatorio (±30 min) para evitar picos en el backend.
- Condiciones: preferir Wi‑Fi, batería > 20% y dispositivo en reposo (configurable por plataforma).
- Failover: si falla la corrida diaria, reintentar con backoff y, al tercer fallo, forzar un full sync del catálogo afectado fuera de pico (con límites de página).
Diagrama de Flujo — Planificador Diario (cliente)
Secuencia — Corrida Programada (diaria)
Ajustes de Endpoint sugeridos
GET /catalogs/{name}/delta?since=ISO8601&cursor=...&pageSize=...- Respuestas con
server_time,version,next_cursor, hints de paginación.
📈 Políticas de Reintento
- Errores 5xx / red: backoff exponencial (2ⁿ segundos, máx 5 min).
- Errores 4xx (409, 412): marcar
FAILED, posible resolución manual. - Timeouts: reducir tamaño de lote dinámicamente.
✅ Decisiones de Diseño por Defecto
| Aspecto | Política |
|---|---|
| Conflictos Catálogos | Servidor gana (LWW) |
| Conflictos Transacciones | Cliente gana (validación posterior) |
| Atomicidad | Por item (no todo-o-nada) |
| Tamaño de lote | 50–200 items |
| Idempotencia (server) | TTL 14 días |
| Sync forzada | Silent push segmentada por device_tag |
📊 Métricas y Observabilidad
- Métricas: tamaño de lote, tasa de conflicto, duración de sync.
- Logs:
idempotency_key,device_id,sync_session_id. - Alertas: spikes de
CONFLICTo latencias elevadas.