WhatsApp / Messaging System
Design a real-time messaging system supporting 1-on-1 and group chats, message delivery status, and online presence.
~9 min readmedium difficultymessaging
PracticeLow Level Design — covers service internals: DB schemas, API contracts, sequence flows, and class interfaces.
These designs are for learning purposes and may not be 100% accurate or production-ready. Always cross-reference with official docs and other resources. Report an issue if you spot something wrong.
Reference Documents
LLD Overview
Low Level Design — WhatsApp Messaging
LLD focus: message persistence with delivery receipts, WebSocket fanout for real-time delivery, presence tracking, and offline push notifications.
Services
- ChatService — send/receive messages, track delivery status (sent → delivered → read)
- ThreadService — manage 1:1 and group threads, E2E encryption key exchange
- PresenceService — heartbeat-based last-seen timestamps in Redis
Data Model
Database Schemas
Cassandra — messages
cql
CREATE TABLE messages (
thread_id UUID,
message_id TIMEUUID, -- encodes sent_at
sender_id UUID,
content BLOB, -- encrypted payload
status VARCHAR, -- sent/delivered/read
PRIMARY KEY (thread_id, message_id)
) WITH CLUSTERING ORDER BY (message_id ASC);
Expand
PostgreSQL — threads + members
sql
CREATE TABLE threads (
thread_id UUID PRIMARY KEY,
is_group BOOLEAN DEFAULT false,
name VARCHAR(100)
);
CREATE TABLE thread_members (
thread_id UUID NOT NULL,
user_id UUID NOT NULL,
joined_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (thread_id, user_id)
);
Expand
Redis — Presence
Diagram
SET last_seen:{userId} {epoch_ms}
-- heartbeat call sets this, client polls on chat open
Expand
API Design
Service Interface Contracts
MessageService
typescript
interface MessageService {
// WebSocket frame: { event: 'send_message', data: SendMessageDto }
sendMessage(senderId: string, dto: SendMessageDto): Promise<MessageAck>;
// WebSocket frame: { event: 'send_media', data: SendMediaDto }
sendMedia(senderId: string, dto: SendMediaDto): Promise<MessageAck>;
// WebSocket frame: { event: 'ack', data: { messageId, status } }
acknowledgeDelivery(recipientId: string, messageId: string, status: 'delivered' | 'read'): Promise<void>;
}
Expand
SessionService
typescript
interface SessionService {
// Called on WebSocket connect; registers server affinity in Redis
openSession(userId: string, serverId: string, deviceId: string): Promise<void>;
// Called on WebSocket disconnect; drains pending messages before closing
closeSession(userId: string, deviceId: string): Promise<void>;
}
Expand
PresenceService
typescript
interface PresenceService {
// REST: PUT /api/v1/presence { status: 'online' | 'offline' }
setOnline(userId: string): Promise<void>;
setOffline(userId: string): Promise<void>;
// GET /api/v1/users/:userId/presence
getLastSeen(userId: string): Promise<PresenceInfo>;
}
Expand
Sequence Diagram
Message Send + Delivery Receipt
Diagram
Expand
LLD Tradeoffs
Design Decisions
| Decision | Options | Chosen |
|---|---|---|
| Message DB | Cassandra vs MySQL | Cassandra — append-only chat workload, efficient range scans by thread |
| Delivery tracking | Client-side vs server-side | Server-side status column — single source of truth, auditble |
| Real-time | WebSocket vs SSE | WebSocket — full-duplex, lower overhead for bidirectional chat |
| Offline delivery | Store-and-forward vs push | Both — messages persisted in Cassandra; push notification wakes app |
Expand
Core Classes
Core Class Interfaces
typescript
class MessageRepository {
// Cassandra partition key: conversation_id, clustering key: message_id (TIMEUUID DESC)
async insertMessage(msg: CreateMessageDto): Promise<Message>;
async getMessagesByThread(conversationId: string, limit: number, beforeId?: string): Promise<Message[]>;
async updateStatus(messageId: string, status: DeliveryStatus): Promise<void>;
async getUndeliveredMessages(userId: string): Promise<Message[]>; // for offline catch-up
}
class DeliveryTracker {
// Status flow: sent → delivered → read
private static readonly TRANSITIONS: Record<DeliveryStatus, DeliveryStatus[]> = {
sent: ['delivered', 'failed'],
delivered: ['read'],
read: [],
failed: ['sent'], // retry
};
async acknowledge(messageId: string, event: 'delivered' | 'read', recipientId: string): Promise<void> {
await this.messageRepo.updateStatus(messageId, event);
await this.notifySender(messageId, event); // WS push tick/double-tick to sender
}
}
class EncryptionService {
// Abstracts Signal Protocol double-ratchet key exchange
async initiateSession(senderId: string, recipientId: string): Promise<SessionKeys>;
async encrypt(plaintext: string, sessionKeys: SessionKeys): Promise<EncryptedPayload>;
async decrypt(payload: EncryptedPayload, sessionKeys: SessionKeys): Promise<string>;
async rotateKeys(sessionId: string): Promise<SessionKeys>; // forward secrecy ratchet step
}
Expand