Design a Chat App
Design one-to-one and group messaging like WhatsApp or Slack: persistent connections, message delivery and ordering, storage per conversation, delivery and read receipts, presence, and offline users.
In a nutshell
- Clients keep a WebSocket to a gateway; a routing layer knows which gateway each online user is connected to.
- Store messages by conversation (partition key) and ordered by a per-conversation sequence or time-ordered ID.
- Delivery is at-least-once with client-generated message IDs for deduplication; receipts are just more messages.
- Offline users get push notifications and fetch missed messages from their last-seen position when they reconnect.
Requirements
- Functional: one-to-one and group chats (up to a few hundred members); text messages; sent/delivered/read receipts; online presence; message history across devices.
- Non-functional: messages delivered in well under a second when both users are online; no message lost once acknowledged; order preserved within a conversation.
Estimates
Assume 100 million daily users sending 40 messages a day: 4 billion messages a day, about 46,000 per second on average and several times that at peak. At 200 bytes per message, that is roughly 800 GB of new messages a day before replication, so storage must scale out. If 20 million users are connected at once and one gateway holds about 100,000 connections, that is around 200 gateway servers.
API
Frames over the WebSocket, plus a history call
-> { "type": "send", "client_msg_id": "c-7f3a", "conversation": 551, "text": "hi" }
<- { "type": "ack", "client_msg_id": "c-7f3a", "seq": 1042 }
<- { "type": "message", "conversation": 551, "seq": 1043, "from": 9, "text": "hello" }
-> { "type": "read", "conversation": 551, "up_to_seq": 1043 }
GET /conversations/551/messages?after_seq=1000&limit=50Data model
| Table | Key | Notes |
|---|---|---|
| messages | partition: conversation_id; clustering: seq | Wide-column store; one slice per conversation |
| conversations | conversation_id | Members, type, last seq |
| user_conversations | user_id | Conversations with last-read seq per user (unread counts) |
| sessions (cache) | user_id → gateway id(s) | Which gateway each online device is connected to |
High-level design
Sending a message between two online users
All steps The chat service stores the message, then routes it to the gateway holding the recipient's connection.
- Asha's phone sends the message over its open WebSocket.
- The chat service assigns the next sequence number in the conversation and stores it; then it acknowledges to Asha.
- It looks up which gateway Ravi is connected to.
- That gateway pushes the message down Ravi's connection. If Ravi is offline, a push notification is sent instead.
Diagram as text
- Asha's phone sends the message over its open WebSocket.
- The chat service assigns the next sequence number in the conversation and stores it; then it acknowledges to Asha.
- It looks up which gateway Ravi is connected to.
- That gateway pushes the message down Ravi's connection. If Ravi is offline, a push notification is sent instead.
Deep dives
Ordering and duplicates
Assign a per-conversation sequence number when the message is stored (a counter in the conversation row, or a time-ordered ID); clients display by it, so everyone sees the same order. The client attaches its own client_msg_id; if it resends after a lost ack, the server recognises the ID and returns the original sequence instead of storing a duplicate.
Offline and multiple devices
Each device remembers the last sequence it has per conversation. On reconnect it asks for everything after that. This also syncs a user's laptop and phone. Push notifications (APNs, FCM) wake offline phones; they carry little or no content, and the app then fetches.
Groups
For small groups, fan out to each online member's gateway. For very large channels, deliver to online members only and let others fetch history when they open the channel, the same pull-on-read idea as celebrity feeds.
Presence
Gateways publish heartbeats; a user is online while their connection is alive. Presence changes are high-volume and low-value: send them only to users who currently have that contact on screen, and debounce flapping connections.
Bottlenecks and trade-offs
- Gateway restarts drop many connections at once: clients reconnect with jittered backoff, and deploys drain gateways gradually.
- Hot conversations (huge groups) concentrate writes on one partition: rate-limit posting, or split very large channels.
- End-to-end encryption means servers store ciphertext only, which limits server-side search and moderation; key management happens on devices.
The last five minutes
Common mistakes
- Ordering messages by client timestamps, which disagree between phones.
- Polling over HTTP for new messages instead of persistent connections.
- Storing messages keyed by message ID, so loading a conversation scatters across shards.
- Not deduplicating resent messages after a lost acknowledgement.
Interview questions
Try answering out loud before you open each one.
How do you guarantee a message is not lost?
The sender's app keeps the message until it receives the server's ack, which is sent only after the message is durably stored (replicated). If no ack arrives it resends with the same client message ID, and the server deduplicates. Recipients fetch from their last sequence on reconnect, so a missed push does not lose anything.
Check yourself
Related topics
Further reading
- The WebSocket API MDN Web Docs
- Firebase Cloud Messaging Firebase documentation
Your notes
Saved in this browser as you type.
Finished this topic?
Mark it read to track your progress, then carry on with the next one.