Diseña el backend de una app de mensajería — recorre la entrega de mensajes, el orden y la presencia en línea
Una sola funcionalidad — "enviar un mensaje" — esconde tres problemas difíciles distintos: entrega duradera, orden por conversación y presencia a gran escala.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña el backend de una app de mensajería — recorre la entrega de mensajes, el orden y la presencia en línea
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.#Delimita el problema antes de dibujar una sola caja
"Diseña un backend de chat" en realidad son tres problemas disfrazados de uno: entrega duradera de mensajes, orden dentro de una conversación y presencia (quién está en línea, quién está escribiendo, quién leyó qué). Una respuesta senior empieza fijando cuál de estos le importa más al entrevistador y planteando requisitos explícitos en vez de adivinar:
- Conversaciones 1 a 1 y grupales, con historial que se sincroniza entre los dispositivos de un mismo usuario.
- Entrega at-least-once — un mensaje nunca debe desaparecer en silencio, aunque el cliente esté desconectado por días.
- Orden consistente dentro de una conversación, no un orden global para todo el sistema.
- Presencia (en línea / desconectado / escribiendo / última conexión) que puede estar ligeramente desactualizada — a nadie le importa si "última vez visto" tiene 2 segundos de retraso.
- Escala: cientos de millones de usuarios, con solo una fracción pequeña conectada de forma concurrente en un momento dado.
Ya hay una asimetría importante: los mensajes necesitan garantías fuertes de durabilidad, la presencia casi ninguna. Tratarlos igual — por ejemplo, escribir cada heartbeat en el mismo almacén duradero que los mensajes — es el error de diseño más común en esta pregunta.
#La forma del sistema
Cuatro piezas hacen casi todo el trabajo: una capa de conexión/gateway que termina conexiones WebSocket persistentes (o HTTP/2 de larga duración), un servicio de mensajes que contiene la lógica de enviar/recibir, un almacén de mensajes que persiste el historial de forma duradera, y un servicio de presencia que registra quién está conectado en este momento. Un registro de conexiones conecta la capa de gateway con todo lo demás — es la respuesta a "¿cómo sabe el servidor B que el usuario X está conectado al nodo de gateway 3 y no al nodo 7?".
#El camino real de un mensaje
- El cliente A envía un mensaje por su WebSocket abierto al gateway al que está conectado, etiquetado con un UUID generado en el cliente (para deduplicar) y el id de la conversación.
- El gateway lo reenvía al servicio de mensajes, que lo agrega de forma duradera al almacén de mensajes antes de hacer cualquier otra cosa — primero durabilidad, después entrega.
- El servicio de mensajes busca al/los destinatario(s) en el registro de conexiones. A los que están en línea se les empuja el mensaje de inmediato por su gateway. A los desconectados se les escribe una entrada en una cola offline por usuario.
- El remitente recibe un ack en cuanto el paso 2 tiene éxito — eso es el check de "enviado". Un segundo ack, generado por el cliente del destinatario, es lo que lo convierte en "entregado".
- Al reconectar, el cliente extrae lo que haya en su cola offline (y/o le pide al almacén "todo desde la secuencia N"), así que un push perdido nunca es un mensaje perdido — solo uno retrasado.
#Orden: por conversación, nunca global
Un orden global para todo el sistema es innecesario y costoso de construir. Lo que realmente importa es que todos los que miran la conversación 42 vean el mismo orden. El truco estándar: enrutar cada mensaje de una conversación dada a través de un único escritor lógico (en la práctica, una partición del almacén de mensajes indexada por conversation_id), y dejar que esa partición asigne un número de secuencia monótonamente creciente al agregar cada mensaje. Como una partición siempre la escribe un solo dueño a la vez, "incrementar y agregar" queda trivialmente ordenado sin necesidad de un lock distribuido.
#Presencia: barata, efímera, y le está permitido equivocarse un poco
La presencia tiene volumen de escritura alto y valor bajo por escritura — un heartbeat cada 15-30 segundos de cada cliente conectado. Guárdala en algo como Redis, con una clave por usuario y TTL corto (presence:user_id expirando, digamos, en 45 segundos); un heartbeat perdido hace que la clave expire sola sin que nadie tenga que ejecutar lógica de limpieza. Cuando una clave se crea o expira, publica un evento pub/sub para que cualquier gateway con un socket abierto de alguien que tenga a ese usuario en sus contactos pueda empujarle la actualización.
Dos refinamientos que un candidato senior suele mencionar por su cuenta: retrasar unos segundos el aviso de "se desconectó" para que un corte breve de red en el metro no haga parpadear el estado del usuario, y nunca escribir la presencia en el mismo almacén duradero y replicado que usas para los mensajes — no necesita esa durabilidad y el volumen de escrituras lo saturaría.
#Fan-out: uno a uno, grupos pequeños, y el canal "celebridad"
Para chats 1 a 1 y grupos pequeños, fan-out en escritura es lo más simple: al enviar un mensaje, se escribe un puntero hacia él en el inbox/timeline de cada miembro en ese mismo momento, así que leer es solo "dame mi inbox", O(1) por cliente. Eso se rompe con un canal de difusión con un millón de suscriptores — escribir un millón de filas de inbox por un solo mensaje es una tormenta de escrituras. Para esos casos, se cambia a fan-out en lectura: el mensaje se guarda una sola vez en el log del canal, y cada cliente suscrito lo consulta desde ese log compartido (con un cursor de "última posición leída" por usuario). La mayoría de los sistemas reales combinan ambas estrategias, eligiendo por conversación según la cantidad de miembros.
#Forma del almacenamiento
| Aspecto | Elección | Por qué |
|---|---|---|
| Clave de partición | conversation_id | Mantiene todos los mensajes de una conversación en un mismo shard, el orden queda local |
| Clave de orden/clustering | message_id (estilo Snowflake) | Único y aproximadamente ordenado en el tiempo, permite range scans baratos para el historial |
| Almacén de presencia | Redis, claves con TTL | Efímero por diseño, no necesita durabilidad |
| Cola offline | Cola/log por usuario | Cierra la brecha entre "enviado" y "entregado" para clientes desconectados |
🧠 Check your understanding
0/3 · 0/3 answered1. ¿Por qué el orden de los mensajes debería definirse por conversación y no de forma global para todo el sistema?
2. ¿Por qué es mala idea usar el reloj de pared para ordenar mensajes dentro de una conversación?
3. ¿Para qué sirve el registro de conexiones?