Diseña un motor de sincronización en segundo plano que envíe de forma confiable las escrituras locales al servidor
Un outbox offline-first, un worker en segundo plano con backoff exponencial y la escalera de resolución de conflictos, desde last-write-wins hasta CRDTs.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña un motor de sincronización en segundo plano que envíe de forma confiable las escrituras locales al servidor
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Aclara los requisitos
La primera pregunta es qué tipo de escrituras. Una app de notas de un solo escritor tiene una superficie de conflicto muy distinta a una lista de compras compartida y editada desde dos dispositivos a la vez. También preguntaría: ¿es obligatorio el offline-first (toda escritura debe sentirse exitosa de inmediato para el usuario, incluso a mitad de camino sin señal), importa el orden de escritura dentro de un mismo registro, qué tan reciente debe ser la vista de un dispositivo par tras sincronizar, y necesitamos soportar los borrados como operaciones de primera clase (un borrado que llega después de una edición concurrente es un caso clásico de conflicto)? Para este diseño asumiré: el offline-first es obligatorio, la mayoría de los conflictos son del mismo usuario en varios dispositivos y no colaboración real multiusuario, y una consistencia eventual de segundos a minutos es aceptable una vez que vuelve la conectividad.
#El patrón central: outbox transaccional
La base es el patrón outbox. Cada mutación local —crear, actualizar, borrar— se escribe en la base de datos local y se añade a una tabla outbox dentro de la misma transacción local. Esto garantiza que una escritura que el usuario cree exitosa nunca pueda perderse en silencio, incluso si la app se cae o el sistema operativo la mata un milisegundo después: o se confirman ambas escrituras, o ninguna. El outbox luego lo drena un worker en segundo plano (WorkManager en Android, BGTaskScheduler / BackgroundTasks en iOS) que es independiente de si el usuario sigue mirando la pantalla.
#Política de reintentos: clasifica antes de reintentar
No todo fallo merece la misma respuesta. Un 5xx, un timeout o una conexión caída es reintentable: la mutación sigue siendo válida, el mundo solo necesita volver a intentarlo, así que vuelve a estado pendiente con backoff exponencial más jitter (el jitter importa a escala: sin él, todos los dispositivos que perdieron conectividad durante el mismo corte reintentan al unísono y golpean al servidor justo cuando se recupera). Un error de validación 4xx es terminal: reintentar un payload inválido para siempre solo gasta batería y nunca tiene éxito, así que tras un pequeño número de intentos pasa a un estado dead-letter que se muestra al usuario o se registra para investigación. Un conflicto de versión 409 no es ninguno de los dos: significa que la mutación era estructuralmente correcta, pero el estado del servidor cambió desde la última vez que el cliente lo vio, y eso es un tercer camino distinto, la resolución de conflictos, que veremos a continuación.
#Resolución de conflictos: elige la estrategia más barata que siga siendo correcta
La mayoría de las apps no necesitan CRDTs, y recurrir a uno por defecto es una trampa común de entrevista. El enfoque correcto es una escalera, de lo más simple a lo más complejo:
- Last-write-wins (LWW), ordenado por un timestamp del servidor o una versión monótonamente creciente. Barato, simple y suficientemente correcto cuando las ediciones concurrentes al mismo campo son raras; el costo es que la escritura perdedora desaparece en silencio, por lo que encaja mal en cualquier cosa que el usuario describiría como "mis datos simplemente desaparecieron".
- Merge a nivel de campo, donde el servidor compara el estado entrante y el existente por campo en lugar de por registro, de modo que dos dispositivos que editan campos distintos del mismo objeto sobreviven ambos. Esto cubre gran parte de los "conflictos" reales, que en realidad son ediciones que no se superponen.
- CRDTs u operational transforms, reservados para superficies genuinamente colaborativas y de alta concurrencia (documentos compartidos, listas compartidas editadas en vivo por varias personas) donde perder una escritura es inaceptable y los merges deben ser automáticos y converger matemáticamente sin importar el orden de llegada.
- Mostrar al usuario, el último recurso cuando el merge automático no puede decidir con seguridad: muestra ambas versiones y deja que una persona elija, en lugar de adivinar mal en silencio algo importante.
#Orden, idempotencia y cómo traer los cambios de vuelta
Dos garantías más hacen que la cola sea confiable. Primero, cada mutación lleva una clave de idempotencia generada por el cliente (un UUID), de modo que si una solicitud se reintenta después de un timeout en el que el servidor sí la procesó, el servidor puede detectar el duplicado y devolver el resultado original en lugar de aplicarla dos veces. Segundo, las mutaciones sobre el mismo registro se drenan en el orden en que se encolaron (un número de secuencia por registro), mientras que las mutaciones sobre registros distintos pueden enviarse en paralelo: quieres garantías de orden exactamente donde importan y no más allá, o sacrificas throughput sin ningún beneficio.
La sincronización no es solo de salida. El motor también necesita traer hacia abajo los cambios del lado del servidor mediante un change feed direccionado por un cursor o un token "desde", en lugar de volver a descargar todo el dataset en cada sincronización. Los deltas remotos entrantes deben reconciliarse contra lo que aún esté en el outbox local: una actualización remota sobre un registro con una mutación local pendiente es exactamente el caso 409 de arriba, solo que descubierto por el lado de la descarga en lugar de por el lado del envío.
#Aspectos operativos que deciden si esto sobrevive al contacto con usuarios reales
La batería y las restricciones de red definen la planificación: las sincronizaciones masivas corren bajo restricciones de WorkManager como red no medida o carga activa, mientras que un guardado disparado por el usuario recibe un intento inmediato de mejor esfuerzo sin importar esas restricciones, porque el usuario está mirando. La observabilidad importa tanto como la corrección: expón la profundidad de la cola, las tasas de error y un indicador simple de estado de sincronización en la UI, porque "mis cambios no se guardaron" es uno de los reportes de bug más difíciles de accionar sin esa telemetría ya en su lugar. Y las apps multi-cuenta necesitan que cada mutación quede etiquetada con la cuenta a la que pertenece, con el outbox vaciado o descartado —nunca enviado en silencio— al cerrar sesión, para que las escrituras pendientes de un usuario jamás se filtren a la sesión de otro en un dispositivo compartido.