Design a chat app
Send a message and it appears on your friend's phone a moment later, in order, exactly once, with a tick when it's delivered and another when it's read, even if their phone was off for a week. Chat looks simple and combines nearly every Level 4 idea: persistent connections, routing between servers, ordering, at-least-once delivery with deduplication, and offline sync. It follows the interview framework phase by phase.
1. The prompt
"Design WhatsApp" or "design Messenger": one-to-one and group chat on phones, with delivery and read receipts.
Most of the difficulty is invisible to users: routing a message between two of hundreds of connection servers, guaranteeing order and no duplicates over flaky mobile networks, and syncing a phone that was off for a week, all while the servers can't read what's being sent.
2. Frame the problem
One-to-one only, or groups too? How big?
Both; groups up to about 1,000 members.
Group delivery is fan-out; the size cap keeps it bounded (and matters for encryption).
Must messages reach people who are offline?
Yes, whenever they come back, plus a push notification.
Messages are stored before delivery, and devices sync by position.
Multiple devices per user?
Yes: a phone plus laptops and tablets.
Each device tracks its own position and has its own encryption keys.
Is the history kept on the servers forever?
Kept, so new devices can load it.
It's the biggest storage decision: petabytes a year versus a small buffer. Worth asking explicitly.
End-to-end encryption?
Yes. Servers must not be able to read message content.
Servers store and forward ciphertext; no server-side search of content.
Receipts, presence and typing?
Delivered and read receipts, online/last-seen and typing indicators.
These are high-volume and soft: they need cheap, lossy handling.
How many users?
500M daily users, 50M connected at peak, 20 billion messages a day.
Hundreds of connection servers and a write-optimised store.
3. What it must do
- A user can send a message to a person or a group, and it's delivered in order, once.
- Offline users get their messages when they return, on every device.
- Users see receipts and presence: delivered, read, online, typing.
Not now:
- Voice and video calls: WebRTC (Module 19).
- Stories and status updates.
- Payments.
- Very large broadcast channels: mentioned in follow-ups.
Photos and videos go to blob storage, and the message carries a reference (Module 10).
4. How well it must do it
| Target | Why this number |
|---|---|
| Fast: under ~200 ms to deliver when both users are online in one region speed | Chat feels like conversation only if it's near-instant. |
| Never lost, never duplicated, in order within a conversation data safety · consistency | A missing or reordered message changes what was said. |
| Scale: 50M concurrent connections, 600K messages a second at peak load shape | New Year's Eve brings huge spikes. |
| Private: end-to-end encrypted security | Users trust the app with their private lives. |
| Works on bad networks: reconnects and catch-up are routine, cheap on battery where it runs | Phones switch networks, sleep and lose signal all the time. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Messages | 2 × 10¹⁰ a day ÷ 10⁵ s | ~200,000/s, ~600,000/s at peak → so a write-optimised, partitioned store |
| Connections | 50M concurrent; ~100–500K per gateway server | → so ~200–500 gateway servers holding WebSockets |
| Storage | 2 × 10¹⁰ × ~200 bytes (encrypted text and metadata) | ~4 TB a day, petabytes a year if kept → so time-bucketed partitions and tiered storage |
| Presence | 50M users, each with ~100 contacts, changing state several times a day | Billions of updates if broadcast → so only to people looking at the conversation |
6. The nouns
- User and Device.
- Conversation: direct or group, with members.
- Message.
- Receipt: delivered or read, up to a position.
- Session: which gateway holds a device's connection right now.
messages partition (conversation_id, time_bucket) · clustering seq desc
client_msg_id, sender_id, encrypted_body, media_ref, sent_at
conversations conversation_id, type, members[], last_seq
devices (user_id, device_id) → push_token, last_acked_seq per conversation
presence online:{user} with a 30 s TTL (soft state, in memory)
7. The contract
A WebSocket while the app is open (both directions), plus HTTPS for sync and media uploads. The user and device come from the authenticated session.
// over the WebSocket, both directions
→ send {client_msg_id: "c9f1…", conversation_id, body}
← ack {client_msg_id: "c9f1…", seq: 1043} (stored: one tick)
← message {conversation_id, seq: 1043, sender, body}
→ receipt {conversation_id, up_to_seq: 1043, type: "delivered" | "read"}
// over HTTPS
GET /v1/conversations/{id}/messages?after_seq=1040&limit=100 (sync after reconnect)
8. First working version
Gateway servers holding WebSockets, a chat service, a message store and a push service.
8.1 Sending to someone online
The chat service stores the message, assigns its sequence number, acknowledges to the sender, then delivers it to the recipient's connection. Version one keeps a table of "which gateway holds which user".
8.2 Someone offline
The message is already stored; the chat service sends a push notification. When the app opens, it fetches messages after its last sequence number.
8.3 Receipts
When Ravi's phone shows the message, it sends "delivered up to 1043"; when he opens the chat, "read up to 1043". The chat service forwards them to Asha like any other message.
Parked for later (section 9 fixes each)
- What decides order when phone clocks disagree? What about resends after a lost ack?
- Keeping "user → gateway" accurate as phones reconnect to different servers.
- Phones with flaky networks miss messages in flight.
- Storage for 20 billion messages a day.
- A message to a 500-person group.
- Presence and typing updates would outnumber real messages.
- Encryption: what can the server see?
9. Make it hold up
9.1 Ordering and duplicates in order · never duplicated
| Approach | Why | |
|---|---|---|
| ✗ Bad | Order by the sender's phone clock | Phone clocks are wrong and differ (Module 20): replies appear before the messages they answer. |
| ~ Good | Order by server receive time | Better, but several servers' clocks disagree too, and ties break unpredictably. |
| ✓ Great | A per-conversation sequence number assigned when the message is stored (one partition per conversation orders its writes), plus the client's own client_msg_id so a resend after a lost ack is recognised and not stored twice | Total order within each conversation, exactly what users perceive; retries are idempotent. No global order is needed. |
9.2 Getting the message to the right server fast · scale
Asha's phone is connected to gateway 12, Ravi's to gateway 87. Gateways are thin: they hold connections and nothing else, so deploying chat logic doesn't drop 50M connections. When Ravi connects, gateway 87 subscribes to his channel in a sharded pub/sub layer (or registers "Ravi → gateway 87" in a session store with a TTL). The chat service stores the message, then publishes it to Ravi's channel; whichever gateway holds Ravi's connection pushes it down (Module 19).
9.3 Offline users, receipts and sync never lost · bad networks
- If Ravi isn't connected, nothing is subscribed: the message is already stored, so the chat service sends a push notification (APNs or FCM) to wake his phone.
- When the app connects, it sends, per conversation, the last sequence number it has, and fetches everything after it: sync by sequence. This also covers flaky networks that drop messages in flight.
- Receipts are just messages flowing the other way: "delivered up to 1043", "read up to 1043". A high-water mark instead of one receipt per message keeps them cheap.
- Each device tracks its own position, so the same history syncs to a phone, a laptop and a tablet.
9.4 Storing billions of messages scale · never lost
Writes dominate, and the read pattern is always "the latest messages in this conversation, then older ones": a perfect fit for a wide-column store such as Cassandra or ScyllaDB (Discord has written about its message storage), partitioned by (conversation_id, time bucket) and clustered by sequence, newest first (Module 13). Buckets keep huge, years-old groups from becoming giant partitions. Some apps keep messages on servers only until all devices have them; others keep full history. That choice changes storage by orders of magnitude, so ask.
9.5 Group chats scale
A message to a 500-member group is stored once in the group's conversation; delivery fans out to the members' channels (and push notifications for offline members). For very large groups or channels, members fetch on demand rather than receiving every message pushed, much like the celebrity case in Design D06. With end-to-end encryption, the sender's device encrypts for each member device (or uses a shared group-key scheme), one reason group sizes are capped.
9.6 Presence and typing scale · bad networks
Presence is soft state: each connection's heartbeat refreshes online:ravi with a 30-second TTL, and expiry means offline. Only send presence changes to people who are looking at the conversation, and throttle typing indicators. At 50M users, unfiltered presence updates would outnumber real messages. Losing a presence update is harmless; the next heartbeat fixes it.
9.7 End-to-end encryption private
With the Signal protocol (used by WhatsApp and Signal), keys live on devices; servers store and forward ciphertext they can't read. The server still knows metadata (who messaged whom, and when) and must deliver to every device of each recipient, since each device has its own keys. This rules out server-side search of message content and shapes how history reaches a new device.
9.8 The whole design after the deep dives
"See you at 7" from Asha to Ravi, then Ravi's laptop catching up later.
10. Interviewer follow-ups
New Year's Eve: 5× the normal messages in one minute.
Gateways are already sized for peak connections; the message path needs headroom in the store and pub/sub. Queue the non-urgent work (push notifications for offline users, receipts) behind real messages, and throttle presence and typing harder during the spike.
A gateway server dies with 200,000 connections.
Phones reconnect (with jittered backoff) to other gateways and resubscribe; then each syncs by sequence, so nothing is lost. Spread reconnects to avoid a stampede, and keep spare gateway capacity.
Broadcast channels with a million followers.
Treat them like a feed, not a chat: store once, don't push every message to every member; members pull when they open the channel, with a light "new messages" nudge.
Users want to search their chat history.
With end-to-end encryption, search runs on the device over its local copy. Without it, a per-user search index fed from the message stream.
Edit and delete messages.
Both are new events in the conversation ("edit seq 1043", "delete seq 1043"), delivered like messages, so every device applies them in order. Storage keeps the latest version (or a tombstone).
Multi-region.
Users connect to their nearest region. Each conversation is homed in one region, which assigns its sequence numbers; members elsewhere receive through cross-region replication with a little extra latency. Home a conversation near most of its members.
How do you fight spam without reading messages?
Metadata and behaviour: account age, sending rate, how many recipients block or report, links shared (where users report them). Rate limits on new accounts and on messages to non-contacts.
Why not long polling?
Long polling works but costs a request per message batch and more battery. WebSockets keep one connection open for both directions. Push notifications cover the app being closed.
Variation: community servers with channels
Another common form: "Design chat for communities, like Discord: servers with many channels, some with hundreds of thousands of members." One-to-one delivery still works, but the unit changes from a conversation to a channel inside a server, and most members of a big server are offline or looking at another channel.
- Fan-out by who is looking. One process per server (sharded by server ID) keeps the server's online sessions in memory. A new message is pushed in full only to sessions that have that channel open or are mentioned. Everyone else gets, at most, a batched "unread" marker.
- Unread without a row per member. Store, per member, the last message ID they read in each channel. A channel is unread when its latest ID is higher. Nothing is written for the 800,000 members who didn't open it.
- Messages by channel and time window. Partition by (channel, time window) with time-ordered IDs (Module 20), so a busy channel never grows one huge partition and history is one range read. A very busy channel needs smaller windows.
- Permissions as roles. A member's rights in a channel come from their roles plus the channel's overrides. The server process caches the result, so each message is checked in memory.
- Member list on demand. The sidebar loads only the part of the member list on screen, grouped by role and online status.
- Voice is a separate system. The chat connection carries the signalling; the audio goes over UDP to media servers that forward each speaker's stream to the listeners. Say so and keep it out of scope unless asked.
Read more: Discord's engineering blog on storing messages by channel and time bucket and on one process per server.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Gateway dies | A few seconds of "connecting…" | Connection count drop | Reconnect with jitter; sync by sequence |
| Pub/sub shard down | Messages arrive late for some users | Publish errors | Recipients' next sync fetches from storage; replicas take over |
| Message store slow | Single tick takes longer | Write latency | Sender retries with the same client_msg_id; no duplicates |
| Push provider outage | Offline users not woken | Push error rate | Messages are stored; delivered on next app open |
| Lost ack on a bad network | Nothing | — | Resend is deduplicated by client_msg_id |
12. Wrap-up and the clock
Trade-offs we made:
- Per-conversation order, not global order.
- Thin gateways and pub/sub routing.
- Store first, then deliver (latency for safety).
- High-water-mark receipts.
- Soft presence.
- End-to-end encryption, at the cost of server-side features.
What we'd watch:
- Send-to-deliver latency p99.
- Connections per gateway.
- Reconnect rate.
- Sync lag.
- Store write latency.
- Push success rate.
At 10×:
- More gateways and pub/sub shards.
- Colder storage tiers.
- Regional homing.
A 45-minute interview
| 0–4 | Frame the problem (groups, offline, history, encryption) |
| 4–9 | Must do, how well, numbers |
| 9–13 | Nouns and contract |
| 13–21 | First working version, parked list |
| 21–40 | Deep dives: ordering (9.1), routing (9.2), sync (9.3), storage (9.4) |
| 40–45 | Follow-ups and wrap-up |
A 60-minute interview
| 0–5 | Frame the problem |
| 5–11 | Must do, how well, numbers |
| 11–16 | Nouns and contract |
| 16–25 | First working version |
| 25–52 | Deep dives 9.1–9.7 and the final picture |
| 52–60 | Follow-ups: gateway failure, channels, regions; wrap-up |
| Level | What a strong answer shows |
|---|---|
| Mid-level | WebSockets, a chat service, a message store, push for offline users, basic receipts. |
| Senior | Routing between gateways via pub/sub, per-conversation sequence numbers and idempotent sends, sync after reconnect, storage partitioning, group fan-out, presence with TTLs. |
| Staff+ | Multi-device and end-to-end encryption implications, very large groups, multi-region homing, reconnect storms, retention as a cost driver, abuse handling without reading content. |
Common mistakes:
- Ordering by timestamps.
- Delivering before storing.
- Relying on pub/sub for offline users.
- One receipt per message.
- Broadcasting presence to everyone.
- Forgetting multiple devices.
13. Try it, check yourself, read more
Send messages between two phones, take one offline, and watch the ticks: ✓ stored on the server, ✓✓ delivered to the other phone, ✓✓ read.
What gives messages a reliable order?
How does a phone catch up after being offline?
Why keep WebSocket gateways thin?
The sender's ack is lost and the phone resends. Why isn't the message stored twice?
| Resource | Why |
|---|---|
| How Discord stores trillions of messages | Message storage partitioning in practice. |
| Signal protocol specifications | How end-to-end encryption works for one-to-one and group messaging. |
| Design WhatsApp (Hello Interview) | Another walkthrough. |