Stage 4 · Provision
System Design Interviews
Design a Chat System
WebSockets, presence, message ordering, offline delivery, and message storage partitions.
Requirements
A chat system enables real-time messaging between users. Messages must be delivered in order, support offline users, and scale to millions of concurrent connections. Key challenge: maintaining persistent WebSocket connections at scale.
Functional:
- 1:1 messaging
- Group messaging (up to 500 members)
- Read receipts, typing indicators
- Online/offline presence
- Message history search
Non-functional:
- Message delivery latency: < 100ms (online users)
- Message ordering: Per-conversation
- Durability: No message loss
- 500M daily active users
- 100B messages per dayThe system must handle 500M daily active users with 100B messages per day (~1M QPS). WebSocket connections must be maintained persistently.
WebSocket Connections
Chat requires persistent connections for real-time delivery. WebSockets provide full-duplex communication. Each user maintains a WebSocket connection to a chat server. Connection state maps users to servers for message routing.
User ──WebSocket──► Chat Server Instance
│
├── User → Server mapping (Redis):
│ user:123 → server-us-east-1a
│ user:456 → server-us-west-2a
│
└── Server → User mapping (local):
server-us-east-1a → [user:123, user:789, ...]
Message routing:
Sender (server-us-east-1a) → Lookup receiver's server → ForwardEach chat server maintains a mapping of which users are connected to it. A global Redis registry maps users to their connected server for cross-server message routing.
Message Flow
1. User A sends message to User B
2. Chat server A receives message
3. Assign monotonically increasing message ID
4. Store message in database
5. Lookup User B's connected server
6. If online: Forward to Chat server B → Deliver via WebSocket
7. If offline: Store in message queue for later delivery
8. Send delivery receipt to User AMessages are assigned sequential IDs for ordering. The sender gets immediate acknowledgment. Delivery to the recipient happens in real-time or via offline queue.
Presence System
Presence tracks which users are online, offline, or away. Each user maintains a heartbeat with their chat server. If the heartbeat stops, the user is marked offline. Presence data is stored in Redis with TTL-based expiration.
# On each heartbeat (every 30 seconds):
def heartbeat(user_id: str):
redis.setex(f"presence:{user_id}", 90, "online")
# TTL of 90 seconds = 3 missed heartbeats = offline
# To check presence:
def is_online(user_id: str) -> bool:
return redis.exists(f"presence:{user_id}")
# Background worker to detect offline users:
def check_stale_presences():
# Scan for keys that have expired
# Publish presence-change events to all interested clientsHeartbeats with TTL provide automatic offline detection. If a user disconnects without sending a goodbye, the TTL expires and presence is updated.
Message Storage
Messages are stored in a database optimized for append-heavy workloads. Cassandra or ScyllaDB are popular choices because they excel at write-heavy workloads and partition data by conversation ID.
CREATE TABLE messages (
conversation_id UUID,
message_time TIMESTAMP,
message_id TIMEUUID,
sender_id UUID,
content TEXT,
PRIMARY KEY (conversation_id, message_time, message_id)
) WITH CLUSTERING ORDER BY (message_time DESC);
-- Query: Get last 50 messages in a conversation
SELECT * FROM messages
WHERE conversation_id = ?
ORDER BY message_time DESC
LIMIT 50;Partition by conversation_id for locality. Clustering by message_time enables efficient range queries for message history.
Offline Delivery
When a recipient is offline, messages are stored in the message queue. When they reconnect, all pending messages are delivered in order. Use a priority queue where recent messages are delivered first to show the most relevant content.
Messages must be ordered within each conversation, not globally. Each conversation gets a monotonically increasing sequence number. This is simpler and more scalable than global ordering.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.