Design WhatsApp & Chat System
WhatsApp-scale messaging — WebSockets, delivery guarantees, group chat fan-out, and presence at 500M+ users.
Interview tip Cover online presence, per-channel message ordering, at-least-once delivery + client idempotency, and fan-out on write vs read for groups.
① Functional requirements
- Send/receive text messages in 1:1 and group conversations
- Message history: paginated fetch on login and scroll-up
- Delivery status: sent → delivered → read receipts
- Online/offline presence and "last seen" (privacy configurable)
- Push notification when recipient offline
- Media attachments (images, files) via separate upload flow
- Group chat: create, add/remove members, admin roles
Out of scope (state in interview)
- Voice/video calls (signaling only mention)
- Full E2E encryption implementation details
- Message editing/deletion sync (brief mention)
- Federation across providers
② Non-functional requirements
- Message delivery latency < 500ms p99 for online users
- 99.9% availability; messages never lost (durable)
- Support 50B messages/day (~580K msg/sec avg)
- Per-conversation ordering guarantee
- End-to-end encryption (mention as optional advanced feature)
③ Back-of-the-envelope scale
Assumptions
- 500M DAU, 50B messages/day → ~580K writes/sec avg, ~2M/sec peak
- Avg message 200 bytes text; 10% with 500KB media metadata
- Storage: 50B × 200B × 365 × 5yr ≈ 1.8 PB text (replication 3× → 5+ PB)
- WebSocket connections: 100M concurrent (20% DAU online)
- Group fan-out: avg 10 recipients × 580K = 5.8M inbox writes/sec
- Presence updates: 100M users × heartbeat/30s ≈ 3.3M updates/sec
Group fan-out dominates write amplification. Hybrid: fan-out on write for groups < 100 members; fan-out on read for larger channels. Presence uses Redis with TTL — approximate is OK.
④ High-level architecture
Chat System Architecture
Mobile / Web Clients
WebSocket Gateway (sticky sessions)
REST API (history, groups)
Chat Service (route, seq IDs)
Presence Service (Redis)
Message Queue (Kafka)
Group Fan-out Workers
Message Store (Cassandra)
Push Service (APNs/FCM)
Media Store (S3 + CDN)
Send msg→Assign seq ID→Persist + fan-out→Push to online clients
Layer 1 — WebSocket Gateway (connection state)
Layer 2 — Chat Service (routing, seq IDs)
Layer 3 — Message Store + Fan-out Queue
Layer 4 — Push / Media / Presence
WebSocket gateway is stateful — sticky load balancing by connection ID. Chat service is stateless. Each conversation has monotonic sequence number assigned by chat service (or per-sender for 1:1).
⑤ Data flow & execution path
End-to-end execution flow
① Client→② API / LB→③ Core services→④ Cache + DB→⑤ Message queue→⑥ Async workers
Sync path: validate → authorize → read/write primary store
Async path: publish domain events → consumers (email, analytics, search index)
Read-heavy path: CDN / edge cache → regional cache → DB replica
Failure path: retry with backoff, DLQ, idempotent handlers
In interviews, trace one user action through this diagram. State what is synchronous (user waits) vs asynchronous (background), and where you enforce idempotency.
⑥ API & interfaces
| Endpoint / flow | Purpose | Notes |
|---|---|---|
| WS: send_message | Real-time send | {convId, clientMsgId, body, type} → ack with serverMsgId + seq |
| WS: message_delivery | Server push | {serverMsgId, seq, sender, body, timestamp} |
| GET /v1/conversations/{id}/messages?before_seq= | History | Paginated 50 msgs; cursor on seq number |
| POST /v1/conversations | Create 1:1 or group | Returns convId; dedupe 1:1 by sorted user pair |
| POST /v1/media/upload | Attachment | Presigned S3 URL; attach mediaId to message |
| GET /v1/users/{id}/presence | Online status | Returns online|offline|last_seen (privacy filtered) |
⑦ Data model & storage
messages (Cassandra, partition by conv_id):
user_inbox (fan-out):
user_devices:
conv_id, seq (clustering), sender_id, body, media_ref, tsuser_inbox (fan-out):
user_id, conv_id, last_seq, unread_countuser_devices:
user_id, device_id, push_token, ws_gateway| Store | What | Why |
|---|---|---|
| Cassandra / HBase | Messages by conversation | Wide-column; partition conv_id; seq ordering native |
| Redis | Presence + unread counts | TTL heartbeat keys; pub/sub for online events |
| Kafka | Fan-out + push events | Durable buffer; replay on consumer failure |
| S3 | Media blobs | Presigned upload; CDN for download |
⑧ Deep dive — core components
Message ordering & idempotency
Per-conversation monotonic
seq assigned by chat service (single partition per conv or lightweight lock). Clients send clientMsgId UUID — server dedupes on (sender_id, clientMsgId) to handle retries. Display order = seq, not client timestamp (clock skew).Delivery: at-least-once over WebSocket. Client acks
serverMsgId; server retries unacked for 30s. Idempotent handler on client prevents duplicate display.Group fan-out strategy
Fan-out on write: For each message, write to each member's inbox table. Fast reads (just read inbox). Write cost = O(group_size). Good for groups < 100.
Fan-out on read: Store message once per conversation; on read, merge conversations user belongs to. Good for large channels. Hybrid: write fan-out for small groups, read fan-out for > 256 members.
WebSocket connection management
100M concurrent connections → ~50K connections per gateway node (2K nodes). Sticky sessions via consistent hash on user_id. Gateway maintains user_id → socket map in memory. On disconnect, presence TTL expires in 60s.
Cross-gateway delivery: chat service publishes to Kafka topic partitioned by user_id; target gateway consumes and pushes to local socket. Alternative: Redis pub/sub per gateway for lower latency.
Offline sync & push
Offline user: message written to inbox; push service sends FCM/APNs with payload {convId, preview}. On reconnect, client calls
GET messages?after_seq=last_known to catch up.New device: full inbox sync from
user_inbox table, then per-conversation history paginated. Compress with gzip; delta sync using seq cursors.⑨ Trade-offs & alternatives
| Decision | Option A | Option B | Pick when |
|---|---|---|---|
| Group delivery | Fan-out on write | Fan-out on read | Write for small groups; read for broadcast channels |
| Ordering | Global seq per conv | Lamport timestamps | Seq simpler; Lamport for multi-master geo |
| Presence | Heartbeat + Redis TTL | Exact connection tracking | TTL approximate; exact costly at scale |
| Storage | Per-user inbox copy | Per-conversation only | Inbox copy for fast read; conv-only saves writes |
| Protocol | WebSocket | Long polling | WebSocket for real-time; polling fallback only |
⑩ 45-minute interview script
- 0–5 min: Clarify 1:1 vs group, receipts, presence, offline behavior
- 5–12 min: Scale — messages/day, concurrent connections, fan-out math
- 12–22 min: Architecture diagram — gateway, chat service, store, push
- 22–32 min: Deep dive ordering, seq IDs, idempotency
- 32–38 min: Group fan-out hybrid strategy
- 38–42 min: Presence, WebSocket scaling, cross-gateway routing
- 42–45 min: Offline sync on new device
⑪ Likely follow-up questions
| Question | Short answer |
|---|---|
| Exactly-once delivery possible? | Practically no over unreliable network; at-least-once + idempotent client is standard |
| How to handle 10K-member group? | Fan-out on read only; store single message; members pull on open |
| Message deleted for everyone? | Tombstone message with deleted flag; push delete event; clients purge locally |
| End-to-end encryption impact? | Server cannot read body; push shows "new message"; key exchange out of band |
| Gateway crashes with 50K connections? | Clients reconnect to another gateway; exponential backoff; resume from last seq |
| Read receipt privacy? | User setting controls send/read; server strips receipt if recipient disabled |
⑫ Revision checklist
- WebSocket gateway with sticky sessions
- Per-conversation sequence numbers
- Client message UUID for dedup
- Fan-out on write vs read trade-off
- Cassandra partitioned by conversation
- Presence via Redis TTL heartbeat
- Push for offline users (FCM/APNs)
- Delivery ack and retry semantics
- Scale: 50B msgs/day math
- Cross-gateway message routing