Diseña el cliente móvil de una app de chat — garantías de entrega, WebSockets, cola offline
Razona sobre la semántica de enviado/entregado/leído, la estrategia de reconexión y el orden de mensajes tal como lo hace un cliente móvil real de WhatsApp/Slack.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña el cliente móvil de una app de chat — garantías de entrega, WebSockets, cola offline
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.#Aclarando los requisitos
El chat es engañosamente simple de demostrar y brutalmente difícil de hacer bien en producción, así que empezaría por precisar exactamente qué garantía estamos prometiendo. Preguntaría: ¿es mensajería uno-a-uno y grupal, o también presencia/indicadores de escritura? ¿Necesitamos confirmaciones de lectura? ¿Cuál es la tolerancia a mensajes duplicados frente a mensajes perdidos? —porque esos dos modos de falla se compensan entre sí, y "nunca perder un mensaje" empuja hacia una entrega al-menos-una-vez, lo cual significa que el cliente debe manejar duplicados. También preguntaría por las garantías de orden dentro de una conversación (¿los mensajes necesitan renderizarse en un orden estricto y consistente en todos los dispositivos de los participantes, incluso bajo particiones de red?) y por el soporte de tamaño de mensaje/adjuntos, ya que los adjuntos grandes suelen manejarse con un pipeline de subida separado en lugar de viajar por el mismo canal que el texto. Para esta respuesta, diseñaré para el estándar habitual: entrega al-menos-una-vez, deduplicación del lado del cliente, orden por conversación, y soporte completo de composición offline — que es la vara con la que WhatsApp, Signal y el cliente móvil de Slack cumplen todos.
#Arquitectura de alto nivel
El trabajo del cliente móvil se divide claramente en tres responsabilidades que es fácil confundir pero que no deberían confundirse: el transporte (cómo se mueven los bytes entre cliente y servidor ahora mismo), el almacén local de mensajes (la fuente de verdad desde la que la UI realmente renderiza — el mismo principio que en el diseño de notas offline: nunca dejes que la UI lea directamente del socket), y el outbox (una cola durable de mensajes que el usuario compuso y que aún no se han confirmado como enviados). Un mensaje que el usuario escribe se guarda en el almacén local y en el outbox dentro de una única transacción local antes de cualquier intento de red — así el mensaje aparece en la UI al instante con un estado de "enviando", sobrevive a que se mate la app, y se reintenta automáticamente en la siguiente ventana de conectividad, sin importar si el WebSocket siquiera estaba conectado en el momento de escribir.
#Garantías de entrega e idempotencia
Diseñaría para entrega al-menos-una-vez en el cable porque es la única opción que sobrevive a redes reales — un cliente puede enviar un mensaje, el servidor puede procesarlo y persistirlo, y el ACK puede perderse antes de llegar al cliente, en cuyo caso el cliente reintenta correctamente, y ahora el servidor ha recibido el mismo mensaje dos veces. La solución no es perseguir exactamente-una-vez a nivel de transporte (es un problema de sistemas distribuidos mucho más difícil de lo que el beneficio justifica aquí); es hacer que la escritura del lado del servidor sea idempotente usando un ID de mensaje generado por el cliente (un UUID creado en el momento de la composición, antes de cualquier interacción con la red). La inserción del servidor es un upsert indexado por ese ID, así que un envío duplicado es un no-op que simplemente devuelve de nuevo el ACK original. Esta única decisión de diseño —claves de idempotencia propiedad del cliente— es lo que permite disparar reintentos de forma agresiva sin necesitar una negociación de "¿ya envié esto?".
#Orden de los mensajes
El orden dentro de una conversación importa más que el orden global entre conversaciones, y es tentador confiar en el tiempo de recepción del servidor, pero eso se rompe en el momento en que dos dispositivos están componiendo casi simultáneamente con latencia de red variable — el orden de llegada al servidor no es lo mismo que el orden de composición, y los usuarios notan cuando una respuesta aparece por encima del mensaje al que responde. Haría que el servidor asigne un número de secuencia monótonamente creciente por conversación en el momento de la escritura durable (no en el momento de recepción), y que cada cliente renderice estrictamente por ese número de secuencia, nunca por timestamp local ni por orden de llegada. Los mensajes compuestos localmente y aún no confirmados se ordenan después del último número de secuencia conocido usando un orden de marcador de posición del lado del cliente (por ejemplo, orden de inserción dentro del bucket "pendiente"), y se reordenan a su posición real una vez que llega el ACK con el número de secuencia autoritativo.
#El ciclo de vida de la conexión WebSocket
Un único WebSocket persistente por sesión de app es el transporte correcto para el camino en vivo — da entrega de baja latencia y permite que el servidor envíe confirmaciones y mensajes nuevos sin hacer polling— pero el cliente tiene que tratar la conexión como fundamentalmente no confiable y temporal: pasar a segundo plano, el cambio de celular a WiFi, y la limpieza de conexiones a nivel de SO la matan constantemente en móvil de formas en que no ocurre en un cliente de escritorio. La estrategia de reconexión necesita backoff exponencial con jitter (para evitar una estampida de reconexiones tras un reinicio del servidor), y —de forma crítica— un protocolo de reanudación/reproducción al reconectar: el cliente le dice al servidor el último número de secuencia que tiene por cada conversación activa, y el servidor reproduce lo que se haya perdido en lugar de que el cliente vuelva a descargar el historial completo. En móvil específicamente, también registraría notificaciones push (APNs/FCM) como camino de entrega de respaldo para cuando la app está en segundo plano o cerrada y el socket directamente no existe — el payload del push puede llevar suficiente información para mostrar una notificación y disparar una descarga/reconexión-y-sincronización en segundo plano cuando la app vuelva a primer plano.
#La cola offline y el outbox en detalle
El outbox es una tabla persistida, no una cola en memoria — los mensajes compuestos estando completamente offline (sin socket, sin red en absoluto) necesitan sobrevivir a que se mate la app y a un reinicio del dispositivo, y aun así enviarse en cuanto vuelva la conectividad. Cada entrada del outbox lleva el ID de mensaje del cliente, la conversación destino, el payload, el timestamp de creación y el estado de reintento (número de intentos, próximo reintento en). Un observador de conectividad (callback de accesibilidad de red) o simplemente un despertar periódico vía una tarea en segundo plano drena el outbox en orden FIFO por conversación cuando una conexión está disponible, respetando el mismo contrato de clave de idempotencia que el camino de envío en vivo — así, "envío offline" y "envío en vivo" son el mismo camino de código con un disparador distinto, no dos implementaciones paralelas que puedan divergir.
#Confirmaciones de lectura, presencia y el costo de la "charla"
Las confirmaciones de lectura y los indicadores de escritura son señales de menor prioridad y mayor frecuencia que los mensajes mismos, y explícitamente les bajaría prioridad y posiblemente los agruparía/coalescería (por ejemplo, un indicador de escritura solo necesita dispararse cada pocos segundos, no en cada pulsación de tecla; una confirmación de lectura para diez mensajes abiertos a la vez puede ser un único evento "leído hasta la secuencia N" en lugar de diez confirmaciones individuales) para que no compitan con la entrega real de mensajes por ancho de banda y batería en una conexión pobre. Este es un buen lugar para reconocer el tradeoff explícitamente en una entrevista: la presencia perfecta en tiempo real es costosa para las radios de un móvil, y un indicador de escritura ligeramente debounced es una degradación completamente razonable y deliberada.
#Cerrando
Si me presionan sobre el mayor riesgo de este diseño, diría que es el protocolo de reconexión/reanudación — es la parte con más probabilidad de tener bugs sutiles (números de secuencia con off-by-one, un mensaje perdido durante una carrera entre el cierre del socket y el drenado del outbox) y la parte más difícil de probar porque requiere simular fallos de red reales en lugar de una prueba unitaria limpia. Invertiría desproporcionadamente en pruebas de integración que maten el socket a mitad de una conversación y verifiquen que el cliente converge al mismo estado que el servidor, porque ese es el escenario que los usuarios reales encuentran constantemente y el que una demo nunca ejercita.