~20 minutes · Day 81 · system design · chat
Tonight's interview design is a WhatsApp-style chat system. The product feels simple: one person sends a message, another person sees it quickly, and the app shows online status and ticks. The interview is not about copying WhatsApp internals. It is about keeping three things separate: the open connection, the durable message, and the tiny side signals around it.
Chat is fan-out over persistent connections: WebSockets hold the pipe open, queues absorb bursts, and presence and receipts live as separate small services.
That sentence is the design. A chat app is not a normal request-response page where the client asks once and leaves. Online clients hold long-lived pipes, messages are persisted before delivery is trusted, and small noisy events like “online” and “read” should not overload the message store.
The system-design script is always: requirements → estimate scale → API → data model → high-level boxes → deep-dive the bottleneck → failure modes. For chat, apply it in that order:
WS /chat, then send events such as send_message, ack_delivered, and ack_read. A WebSocket gives a long-lived two-way session (MDN WebSocket API).users, conversations, members, messages, and delivery_state. Keep presence separate with short TTLs.client -> server: send_message { conversation_id, client_msg_id, body }
server -> client: message_saved { message_id, server_time }
server -> peer: message { message_id, conversation_id, body }
peer -> server: ack_delivered { message_id }
The client_msg_id matters because mobile networks retry. If the same send arrives twice, the server can recognize it and avoid storing two copies.
The bottleneck is fan-out. One message becomes delivery work for every recipient, and that work must respect ordering without letting a slow or offline recipient block everyone else.
| Choice | How it works | Trade-off |
|---|---|---|
| Fan-out on write | When the sender writes, create delivery jobs for each recipient. | Fast recipient delivery and simple unread counts; expensive for very large groups. |
| Fan-out on read | Store one message; recipients fetch unread messages when they open the app. | Cheap writes; slower reads and weaker real-time delivery. |
| Hybrid | Use write fan-out for one-to-one and small groups; use read or batched fan-out for huge groups. | Best practical default, but more rules to operate and explain. |
For this lesson, choose fan-out on write for one-to-one and small groups. Persist the message first, enqueue delivery per recipient, then let workers push through the gateway that owns each online user's WebSocket. If a user is offline, the job waits or becomes unread state. If a worker retries, message_id and recipient_id make delivery idempotent.
Ordering is the quiet detail to say out loud. Put messages for the same conversation on the same ordering key or queue partition, because systems such as Kafka preserve order inside a partition (Kafka concepts). Cross-conversation global ordering is not worth chasing.
| Figure | Why it matters | Source |
|---|---|---|
| 1 open connection per online device | WebSocket gateways scale by concurrent sockets, not just requests/sec. | RFC 6455 |
| 100 ms | A good target for a message to feel instant in the UI. | NN/g response limits |
| 10k messages/sec × 1 KB ≈ 10 MB/sec | Back-of-the-envelope sizing before replication, indexes, and receipts. | Derived estimate |
| ~200 µs Redis round trip | Presence can live in fast ephemeral storage, but each network hop still costs something. | Redis latency guide |
| 1 ordering key per conversation | Keep a chat's messages ordered without forcing one global order. | Kafka concepts |
1. A chat gateway CPU is fine, but messages arrive in bursts. What absorbs the burst without dropping messages?
2. A user sends one message to a small group. Which fan-out choice makes recipients receive fastest?
3. Why keep presence separate from the message store?
4. A recipient is offline. What should the system do after storing the message?
Redraw the architecture diagram from memory on paper: mobile clients, edge/load balancer, WebSocket gateways, chat service, message store, delivery queue, workers, presence service, and receipt service. Then explain the design out loud in 5 minutes as if to an interviewer.
Your explanation must use the seven-step script and must say the invariant from this page without looking. No LeetCode tonight. For the encode step in your Notion tracker, write one sentence explaining why chat separates persistent connections, queues, presence, and receipts.
Read MDN — The WebSocket API after your redraw, as the compare step. Focus on why the open two-way connection changes the shape of the server.