Design WhatsApp
The interviewer asks
“Design a messaging app like WhatsApp: one-to-one and group chat, delivery and read receipts (ticks), online/last-seen, media sharing, and end-to-end encryption, for over a billion users on mobile networks of very mixed quality.”
What a strong answer looks like
A strong answer is built around a persistent-connection layer and store-and-forward delivery. Each phone keeps one long-lived connection to a chat gateway; a session registry maps user (and device) to the gateway holding it. Sending a message means persisting it to the recipient's per-device inbox, acknowledging the sender (one tick), routing it to the recipient's gateway if online (two ticks after the device acks), or sending a push notification if offline. Messages are end-to-end encrypted with the Signal protocol, so the server only stores and forwards ciphertext and deletes it after delivery. Ordering is per conversation using sequence numbers, retries are made safe with client message IDs, groups use fan-out per member device with sender keys, and media goes to blob storage with only an encrypted reference in the message.
Concepts to mention
1 Requirements
Functional
- One-to-one text messages with sent / delivered / read ticks
- Group chats up to 1,024 members
- Offline delivery: messages wait on the server until the device comes online
- Media: images, video, voice notes, documents
- Online status and last seen, typing indicator
- End-to-end encryption; multi-device (phone + up to 4 linked devices)
Non-functional
- 2B users, ~500M concurrently connected at peak
- ~100B messages/day; peaks 3-5x on New Year's Eve and Diwali
- Delivery latency p99 under 500 ms when both users are online in the same region
- No message loss once the sender sees one tick; no duplicates shown to users
- Per-conversation ordering; works on 2G/3G and flaky networks with small battery cost
- Server must not be able to read message content
Out of scope (say it out loud)
- Voice and video calls (separate WebRTC design)
- Payments (UPI in WhatsApp) and business APIs
- Status / stories
- Spam and abuse detection details
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
WS/chat (persistent, binary protocol) | Single long-lived connection carrying send, deliver, ack, receipt, presence and typing frames |
WS framesend {client_msg_id, conv_id, to, ciphertext_per_device} | Send a message; server replies ack {client_msg_id, server_seq} = one tick |
WS framedeliver / ack {msg_id} | Server pushes message to device; device acks so the server can mark delivered and delete it |
WS framereceipt {msg_id, type: delivered|read} | Ticks sent back to the original sender, also end-to-end encrypted metadata where possible |
GET/v1/keys/{user_id} | Fetch identity key, signed pre-key and a one-time pre-key bundle for each device to start an encrypted session |
POST/v1/media | Upload encrypted media blob; returns media_id/URL included inside the encrypted message |
POST/v1/groups/{group_id}/members | Add or remove group members (admin only) |
4 Data model
inbox (pending messages)Cassandra / HBase-style wide-column storedevice_id (partition)server_seq (clustering)msg_idfromconv_idciphertextcreated_atTTL 30 daysStore-and-forward queue per recipient device; rows deleted after the device acks. Write-heavy and append-only, which suits LSM-tree stores.
session registryRedis cluster (or in-memory distributed registry)device_id → gateway_idconnected_atUpdated on connect/disconnect with a TTL refreshed by heartbeats so stale entries expire.
users and devicesMySQL/Postgres sharded by user_iduser_idphone (hashed lookup)devices[]push_tokensprivacy settingsRelational, moderate volume; phone-to-user lookup for contact discovery.
public keysSharded key-value storedevice_id → identity keysigned pre-keyone-time pre-keys listOne-time pre-keys are consumed atomically; client tops up when low.
groupsSharded SQL or Cassandragroup_idmembers (user_id, role)versioncreated_atMember list cached in the group fan-out service; version used to detect stale caches.
mediaBlob storage (S3-like) + CDNmedia_id → encrypted blobsizeexpiryServer stores only ciphertext; decryption key travels inside the E2E encrypted message.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Send a 1:1 message (recipient online)
- 1Sender's app encrypts the message once per recipient device (and per own linked device) using the Signal session and sends a send frame with client_msg_id
- 2Gateway forwards to the message router, which assigns a server_seq for the recipient device inbox and writes the ciphertext to Cassandra
- 3Only after the write succeeds, the router acks the sender: one grey tick
- 4Router looks up the recipient device in the session registry and forwards the message to that gateway
- 5Gateway pushes it down the recipient's connection; the device stores it locally and sends ack
- 6Router deletes it from the inbox and sends a delivered receipt to the sender: two grey ticks
- 7When the user opens the chat, the device sends a read receipt: two blue ticks (if read receipts are enabled)
Recipient offline
- 1Router finds no live session (or the gateway reports the connection gone)
- 2Message stays in the inbox; the router enqueues a push notification with no content (or encrypted preview) to FCM/APNs
- 3When the phone reconnects, the gateway asks the router for all inbox rows with server_seq greater than the device's last acked seq
- 4Messages are delivered in order in batches; each batch ack deletes them and triggers delivered receipts to senders
Send to a group of 500
- 1Sender uses a sender key (Signal 'sender keys'): it encrypts the message once with its group chain key; the sender key was earlier shared with each member device over pairwise sessions
- 2Server receives one ciphertext plus the group_id; the group service expands membership to ~500 users × devices
- 3Router writes one inbox entry per member device (or one shared message row plus per-device pointers) and delivers to online devices
- 4Delivery receipts are aggregated per message so the sender sees 'delivered to all' without 500 separate notifications
7 Deep dives
Connections, routing and the session registry
Mobile phones cannot be reached by inbound connections, so each device opens one long-lived TCP/TLS connection (WhatsApp historically used a compact XMPP-derived binary protocol; WebSocket or MQTT also work) to a gateway chosen by the load balancer. Gateways are only connection holders: they authenticate, keep heartbeats, and forward frames. Each connect writes device_id → gateway_id into a session registry with a TTL refreshed by heartbeats, so a crashed gateway's entries expire. The router, given a recipient device, reads the registry and sends the frame to that gateway over an internal RPC or message bus. A gateway can hold hundreds of thousands to about a million mostly idle connections; WhatsApp famously ran around 2M connections per Erlang server. Heartbeat interval is a battery-vs-staleness trade-off: send small pings every few minutes, and adapt to the network (NAT timeouts on Indian mobile carriers can be short). Because connections drop constantly on mobile, the design must assume any delivery can fail mid-way, which is why delivery is ack-based from the inbox rather than fire-and-forget.
Delivery guarantees, ordering and dedup
The promise to the user is: once you see one tick, the message will arrive exactly once, in order. 'Exactly once' over a network is really at-least-once delivery plus idempotent handling. The sender attaches a client_msg_id (UUID); if its send times out and it retries, the router finds the existing record and returns the same ack instead of writing twice. The server persists before acking, so a crash after the ack cannot lose the message. Towards the recipient, the router may deliver a message twice (for example if the device's ack was lost); the device dedups by msg_id. Ordering is per recipient device inbox: server_seq is monotonic per inbox (assigned by the partition owner or using a per-inbox counter), and the device applies messages in seq order and asks for gaps. Global ordering across conversations is not needed. Within a conversation, two people typing at once can produce interleavings that differ slightly between devices; using the sender's timestamp plus server_seq as a tie-break is a standard, acceptable answer. Deletion after ack keeps server storage tiny, which is also a privacy feature.
End-to-end encryption with the Signal protocol
With E2E encryption the server is a blind relay. Each device has a long-term identity key pair, a signed pre-key, and a batch of one-time pre-keys, whose public parts are uploaded to the key service. To start a session, the sender fetches the recipient device's pre-key bundle and runs X3DH to derive a shared secret without the recipient being online. Each message then uses the Double Ratchet, which gives forward secrecy (old keys cannot be derived from new ones) and post-compromise recovery. For groups, each member creates a sender key and distributes it to every other member device via pairwise sessions; group messages are then encrypted once, which makes server fan-out cheap. Consequences for system design: the server cannot do search, spam filtering on content or server-side message history; multi-device means encrypting per device, so a user with 5 devices costs 5 ciphertexts; adding a device requires a key-change notice ('security code changed'); and backups must be encrypted separately (by a user key or password-protected backup keys). Media is encrypted on the device with a random key; the blob goes to storage and the key and hash travel inside the encrypted message.
Presence and last seen without melting the system
Presence looks small but is a fan-out trap: if every status change were pushed to every contact, a user with 500 contacts would create 500 events each time they open the app, and with hundreds of millions of users it explodes. Use pull-on-demand plus subscriptions limited to open chats. When you open a chat with someone, your device subscribes to their presence; the presence service keeps online state from gateway heartbeats in a sharded in-memory store with a TTL and notifies only current subscribers. Last-seen timestamp is written on disconnect and read lazily. Typing indicators are ephemeral frames routed directly to the other party's gateway with no persistence; if lost, nobody cares. Privacy settings (hide last seen from everyone or non-contacts) are checked in the presence service before returning anything. Showing that presence and typing are best-effort and transient while messages are durable is a key signal of judgement.
8 Trade-offs
Store-and-forward with deletion vs permanent server history
WhatsApp keeps messages on the server only until delivered; history lives on devices plus encrypted backups. That keeps storage small and supports E2E privacy, but new devices cannot simply download history from the server. Slack and Telegram cloud chats choose server history for search and multi-device sync at the cost of the server seeing content.
Fan-out on write for groups
Writing one inbox entry per member device makes reads trivial and works because groups are capped at ~1,024. For huge broadcast channels you would switch to fan-out on read: store once per channel and let devices pull.
Custom binary protocol vs plain WebSocket+JSON
A compact binary protocol saves bytes and battery on 2G/3G, which matters for Indian users with low-end phones and expensive data. WebSocket with protobuf is a good modern equivalent; JSON is fine for an MVP but wasteful at this scale.
Cassandra for inboxes vs a relational DB
Inbox traffic is append-then-delete at millions of writes per second, partitioned by device. An LSM-based wide-column store handles this well; the cost is tombstones from deletions, which needs tuned compaction and short gc_grace.
9 Bottlenecks
New Year's Eve midnight spike
Messages per second jump several-fold within minutes, mostly greetings with media. Pre-scale gateways and routers, let queues absorb the burst, prioritise text over media downloads, and shed non-critical work like presence updates and typing indicators first.
Large groups and viral forwards
One forward into many 1,000-member groups creates huge fan-out. Cap group size, rate-limit forwards (WhatsApp limits forwarding to 5 chats, and 'highly forwarded' messages to 1 chat at a time), and deduplicate media blobs by hash so the same video is stored once.
Inbox tombstones
Billions of deletes per day create tombstones that slow reads in LSM stores. Use time-bucketed partitions or TTL-based expiry, and compaction strategies suited to queue-like data.
10 Failure handling
Gateway crash
Its devices reconnect elsewhere within seconds and resync from their last acked server_seq. Registry entries for the dead gateway expire via TTL, and any in-flight messages remain in the inbox because they were persisted before delivery.
Router fails after persisting but before acking the sender
The sender times out and retries with the same client_msg_id; the router finds the existing record and acks. No duplicate.
Data centre outage
Users are homed to a region, with inbox data replicated to a second region. On failover, devices reconnect to the other region; a small window of unreplicated messages may be retried from senders' unacked outboxes, which is why clients keep unacked messages until they get one tick.
11 What impresses
- Explaining exactly what each tick means in terms of server state
- Saying 'at-least-once plus dedup' instead of claiming exactly-once
- Knowing how E2E encryption changes the design: no server search, per-device ciphertexts, sender keys for groups
- Treating presence and typing as ephemeral and not persisting them
- Designing for flaky mobile networks: resume from last acked seq, small binary frames
- Deleting messages after delivery to keep storage and privacy risk low
12 Mistakes
- Using HTTP polling for message delivery
- Acking the sender before the message is durably stored
- Requiring global ordering of all messages
- Broadcasting presence changes to all contacts
- Sending media bytes through the chat connection
- Saying 'the server encrypts messages' when asked about E2E encryption
13 Follow-ups
Lessons this design uses
Practice questions
- Model the messages table for a chat app on Cassandra. Users open a conversation and scroll back through history.
- You store chat messages in Cassandra with partition key chat_id. Some group chats have years of history and millions of messages, and reads on them are slow and nodes are unbalanced. How do you redesign the key?
- Your chat app shows users as online for minutes after they lose network in a metro tunnel. Why doesn't the server notice, and how do you detect dead connections?
- Write down the reconnect logic you want in your mobile client for a real-time connection. What should it do when the connection drops?
- User A is connected to gateway server 7 and user B to gateway server 42. A sends B a chat message. How does the message reach B?
- Design the online/last-seen presence feature for a messaging app with 50 million daily users. How do you keep it accurate without overloading the system?
- Estimate the scale for a WhatsApp-like chat app with 500 million daily users: messages per second, storage, and concurrent connections.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.