Skip to content
advancedPhase 52 · HLD Case Studies

WhatsApp / Chat System

Design a real-time messaging system with presence and delivery.

2h
0 problems
Topic Progress0%

Requirements & Scope

Functional Requirements

Feature Description
1:1 Messaging Send and receive text, images, videos, documents, voice messages
Group Messaging Groups up to 1024 members with admin controls
Media Sharing Images, videos, documents, voice notes, contacts, locations
Online/Offline Status Show when users are online, last seen timestamp
Read Receipts Double checkmark (delivered), blue checkmark (read)
Typing Indicators Show when the other person is typing
Push Notifications Notify offline users of new messages
Message Sync Sync messages across multiple devices
End-to-End Encryption Messages encrypted so only sender/receiver can read

Non-Functional Requirements

Requirement Target
Low Latency Messages delivered in < 200ms (p99) for online users
Message Ordering Strict per-conversation ordering
Offline Support Messages queued and delivered when user comes online
High Availability 99.99% uptime
Message Durability Messages never lost once sent (at-least-once delivery)
Scale 2 billion MAU, 100 billion messages/day, 50 million messages/sec peak
E2E Encryption Signal Protocol — server cannot read message content

Capacity Estimation

Traffic:
  MAU: 2 billion
  DAU: ~1.5 billion
  Messages/day: 100 billion = ~1.16 million msgs/sec
  Peak: 50 million msgs/sec (during holidays/events)
  Avg message size: 100 bytes text + 100KB media (10% of messages)

Storage (per day):
  Text: 100B msgs × 100 bytes = 10 TB/day
  Media: 10B msgs × 100KB = 1 PB/day (most in S3)
  With 3x replication: 30 TB text, 3 PB media

Connections:
  Concurrent connections: ~500 million
  WebSocket memory: 500M × 10KB = 5 TB RAM for connections

Bandwidth:
  50M msgs/sec × 100 bytes = 5 GB/sec (text only)
  With media: 50 GB/sec peak

Message Storage & Delivery

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                         WHATSAPP CHAT ARCHITECTURE                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌──────────┐                                                              │
│  │ Sender   │                                                              │
│  │ Device   │                                                              │
│  └────┬─────┘                                                              │
│       │ WebSocket                                                           │
│       ▼                                                                     │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                  │
│  │ Connection   │───▶│ Chat         │───▶│ Message      │                  │
│  │ Server       │    │ Service      │    │ Queue        │                  │
│  │ (WS Gateway) │    │              │    │ (Kafka)      │                  │
│  └──────────────┘    └──────┬───────┘    └──────┬───────┘                  │
│                             │                    │                          │
│                             ▼                    ▼                          │
│                      ┌──────────────┐    ┌──────────────┐                  │
│                      │ Storage      │    │ Push         │                  │
│                      │ Service      │    │ Notification │                  │
│                      │ (Cassandra)  │    │ Service      │                  │
│                      └──────────────┘    └──────┬───────┘                  │
│                                                  │                          │
│                                                  ▼                          │
│                                          ┌──────────────┐                  │
│                                          │ APNS / FCM   │                  │
│                                          └──────────────┘                  │
│                                                                             │
│  ┌──────────────┐                                                          │
│  │ Receiver     │◀──── Connection Server (if online)                        │
│  │ Device       │◀──── Push Notification (if offline)                       │
│  └──────────────┘                                                          │
└─────────────────────────────────────────────────────────────────────────────┘

Connection Management

class ConnectionManager:
    """Manages WebSocket connections from clients."""

    def __init__(self):
        self.connections: Dict[str, WebSocket] = {}  # user_id → WebSocket
        self.user_servers: Dict[str, str] = {}       # user_id → server_id
        self.redis = RedisCluster()

    async def handle_connection(self, websocket: WebSocket, user_id: str):
        """Handle new WebSocket connection."""
        # Register connection
        self.connections[user_id] = websocket
        self.user_servers[user_id] = self.server_id

        # Register in Redis for routing
        self.redis.set(
            f"connection:{user_id}",
            json.dumps({
                'server_id': self.server_id,
                'connected_at': time.time()
            }),
            ex=300  # 5 min TTL, refreshed by heartbeat
        )

        # Update presence
        await self.update_presence(user_id, 'online')

        # Start heartbeat
        asyncio.create_task(self.heartbeat_loop(user_id, websocket))

        # Deliver queued messages
        await self.deliver_queued_messages(user_id)

    async def heartbeat_loop(self, user_id: str, websocket: WebSocket):
        """Send periodic heartbeats to detect disconnections."""
        while True:
            try:
                await websocket.send_json({'type': 'heartbeat', 'ts': time.time()})
                # Refresh Redis TTL
                self.redis.expire(f"connection:{user_id}", 300)
                await asyncio.sleep(60)  # Every 60 seconds
            except Exception:
                await self.handle_disconnect(user_id)
                break

    async def handle_disconnect(self, user_id: str):
        """Handle user disconnection."""
        self.connections.pop(user_id, None)
        self.user_servers.pop(user_id, None)
        self.redis.delete(f"connection:{user_id}")
        await self.update_presence(user_id, 'offline')

Message Flow

class ChatService:
    """Core message handling service."""

    def __init__(self):
        self.connection_manager = ConnectionManager()
        self.message_store = MessageStore()
        self.message_queue = KafkaProducer()
        self.notification_service = NotificationService()

    async def send_message(self, sender_id: str, message: Message) -> str:
        """Send a message from sender to receiver(s)."""
        # Step 1: Validate sender
        if not await self.validate_sender(sender_id):
            raise UnauthorizedError("Invalid sender")

        # Step 2: Generate message ID and sequence number
        message_id = str(uuid.uuid4())
        sequence = await self.get_next_sequence(
            message.conversation_id
        )

        # Step 3: Create message record
        msg_record = {
            'message_id': message_id,
            'sender_id': sender_id,
            'conversation_id': message.conversation_id,
            'content': message.content,
            'content_type': message.content_type,
            'sequence': sequence,
            'timestamp': time.time(),
            'status': 'sent'
        }

        # Step 4: Persist to storage (async)
        await self.message_store.save(msg_record)

        # Step 5: Route to recipient(s)
        recipients = await self.get_recipients(
            message.conversation_id, sender_id
        )

        for recipient_id in recipients:
            # Check if recipient is online
            connection_info = self.connection_manager.get_connection(
                recipient_id
            )

            if connection_info:
                # Online: deliver directly via WebSocket
                await self.deliver_message(recipient_id, msg_record)
            else:
                # Offline: queue for delivery + push notification
                await self.message_queue.send(
                    topic='offline_messages',
                    key=recipient_id,
                    value=json.dumps(msg_record)
                )
                await self.notification_service.send_push(
                    recipient_id,
                    msg_record
                )

        # Step 6: Send delivery acknowledgment to sender
        return message_id

    async def deliver_message(self, user_id: str, message: dict):
        """Deliver message to an online user."""
        connection = self.connection_manager.get_connection(user_id)
        if connection:
            await connection.send_json({
                'type': 'message',
                'data': message
            })
            # Update status to 'delivered'
            await self.message_store.update_status(
                message['message_id'], 'delivered'
            )

Message Storage (Cassandra)

-- Messages table: partition by conversation_id, cluster by timestamp
CREATE TABLE messages (
    conversation_id UUID,
    message_id UUID,
    sender_id UUID,
    content TEXT,
    content_type TEXT,  -- 'text', 'image', 'video', 'document'
    sequence BIGINT,
    timestamp TIMESTAMP,
    status TEXT,        -- 'sent', 'delivered', 'read'
    PRIMARY KEY (conversation_id, timestamp, message_id)
) WITH CLUSTERING ORDER BY (timestamp DESC)
  AND default_time_to_live = 31536000;  -- 1 year retention

-- Conversation index: track user's conversations
CREATE TABLE user_conversations (
    user_id UUID,
    conversation_id UUID,
    last_message_time TIMESTAMP,
    last_message_preview TEXT,
    unread_count INT,
    PRIMARY KEY (user_id, last_message_time, conversation_id)
) WITH CLUSTERING ORDER BY (last_message_time DESC);

-- Message status tracking
CREATE TABLE message_status (
    message_id UUID,
    user_id UUID,
    status TEXT,       -- 'sent', 'delivered', 'read'
    status_time TIMESTAMP,
    PRIMARY KEY (message_id, user_id)
);

Message Queue (Kafka)

# Kafka configuration for offline message delivery
KAFKA_CONFIG = {
    'bootstrap_servers': ['kafka-1:9092', 'kafka-2:9092', 'kafka-3:9092'],
    'topic': 'offline_messages',
    'partitions': 128,  # Parallelism for offline delivery
    'replication_factor': 3,
    'retention_hours': 24,  # Keep offline messages for 24h
    'consumer_groups': {
        'offline_delivery': {
            'group_id': 'offline-delivery-workers',
            'auto_offset_reset': 'latest'
        }
    }
}

# Worker processes offline messages
class OfflineMessageWorker:
    def process(self, message):
        data = json.loads(message.value)
        recipient_id = data['recipient_id']

        # Check if user is now online
        connection = self.connection_manager.get_connection(recipient_id)
        if connection:
            # User came back online — deliver directly
            await connection.send_json({
                'type': 'message',
                'data': data
            })
        else:
            # Still offline — keep in queue for later
            # Message already persisted to Cassandra
            pass

Real-time Communication

WebSocket Protocol

┌─────────────────────────────────────────────────────────────────────┐
│                    WEBSOCKET COMMUNICATION FLOW                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Client                          Server                            │
│    │                               │                               │
    │──── HTTP Upgrade ─────────────▶│                               │
    │    GET /ws?token=xxx           │                               │
    │                               │                               │
    │◀──── 101 Switching Protocols ──│                               │
    │                               │                               │
    │◀══════ WebSocket Connection ═══│                               │
    │                               │                               │
    │──── { type: "auth" } ────────▶│  Authenticate                 │
    │◀─── { type: "auth_ok" } ──────│                               │
    │                               │                               │
    │──── { type: "message", } ────▶│  Send message                 │
    │    { content: "Hello" }       │                               │
    │                               │───▶ Route to recipient        │
    │                               │                               │
    │◀─── { type: "delivery_ack" } ─│  Acknowledge delivery         │
    │                               │                               │
    │◀══════ { type: "message" } ════│  Receive message              │
    │    { content: "Hi!" }         │                               │
    │                               │                               │
    │──── { type: "read_ack" } ────▶│  Mark as read                 │
    │                               │                               │
    │──── { type: "typing" } ──────▶│  Typing indicator             │
    │◀══════ { type: "typing" } ═════│  Other user typing            │
    │                               │                               │
    │──── { type: "heartbeat" } ───▶│  Keep alive                   │
    │◀─── { type: "heartbeat_ack" } │                               │

Message Types

type WSMessage =
  | { type: "auth"; token: string; device_id: string }
  | { type: "auth_ok"; user_id: string; server_time: number }
  | { type: "message"; conversation_id: string; content: string; content_type: string; client_msg_id: string }
  | { type: "message_ack"; message_id: string; status: "sent" | "delivered" | "read" }
  | { type: "delivery_ack"; message_id: string; server_msg_id: string; timestamp: number }
  | { type: "typing"; conversation_id: string; is_typing: boolean }
  | { type: "read_receipt"; conversation_id: string; last_read_sequence: number }
  | { type: "presence"; user_id: string; status: "online" | "offline"; last_seen: number }
  | { type: "heartbeat"; ts: number }
  | { type: "heartbeat_ack"; ts: number }
  | { type: "error"; code: string; message: string }

Message Ordering with Sequence Numbers

class MessageSequencer:
    """Ensures strict per-conversation message ordering."""

    def __init__(self):
        self.redis = RedisCluster()

    async def get_next_sequence(self, conversation_id: str) -> int:
        """Get next sequence number using Redis atomic increment."""
        key = f"seq:{conversation_id}"
        # Atomic increment — guaranteed unique even with concurrent requests
        sequence = self.redis.incr(key)
        # Set TTL in case conversation becomes inactive
        self.redis.expire(key, 86400 * 30)  # 30 days
        return sequence

    def get_client_sequence(self, client_msg_id: str) -> int:
        """Map client-side message ID to server sequence."""
        # Client generates message_id locally before network
        # Server maps it to server-side sequence on receipt
        mapping = self.redis.get(f"client_seq:{client_msg_id}")
        if mapping:
            return int(mapping)
        return None

Client-Side Optimistic UI

class ChatClient {
    sendMessage(conversationId, content, contentType) {
        // Generate client message ID before sending
        const clientMsgId = generateUUID();

        // Add to UI immediately (optimistic)
        this.ui.addMessage({
            id: clientMsgId,
            status: 'sending',
            content: content,
            timestamp: Date.now()
        });

        // Send via WebSocket
        this.ws.send({
            type: 'message',
            conversation_id: conversationId,
            content: content,
            content_type: contentType,
            client_msg_id: clientMsgId
        });

        // Handle acknowledgment
        this.on('delivery_ack', (msg) => {
            if (msg.client_msg_id === clientMsgId) {
                this.ui.updateMessageStatus(clientMsgId, 'sent');
                this.ui.updateMessageId(clientMsgId, msg.server_msg_id);
            }
        });

        // Handle failure
        this.on('error', (err) => {
            if (err.client_msg_id === clientMsgId) {
                this.ui.updateMessageStatus(clientMsgId, 'failed');
                // Offer retry
            }
        });
    }
}

Retry & Deduplication

class MessageDeduplicator:
    """Prevent duplicate messages from retries."""

    def __init__(self):
        self.redis = RedisCluster()
        self.DEDUP_TTL = 86400  # 24 hours

    def is_duplicate(self, client_msg_id: str) -> bool:
        """Check if message was already processed."""
        key = f"dedup:{client_msg_id}"
        # SETNX returns True if key was set (not duplicate)
        result = self.redis.setnx(key, '1')
        if result:
            self.redis.expire(key, self.DEDUP_TTL)
            return False  # Not a duplicate
        return True  # Duplicate — skip processing

    def get_server_message_id(self, client_msg_id: str) -> str:
        """Get server-assigned message ID for client's message."""
        return self.redis.get(f"server_msg:{client_msg_id}")

Presence & Read Receipts

Presence System

┌─────────────────────────────────────────────────────────────────────┐
│                       PRESENCE ARCHITECTURE                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────┐    ┌──────────────┐    ┌──────────────┐             │
│  │ Client   │───▶│ Connection   │───▶│ Presence     │             │
│  │          │    │ Server       │    │ Service      │             │
│  └──────────┘    └──────────────┘    └──────┬───────┘             │
│                                              │                      │
│                                              ▼                      │
│                                       ┌──────────────┐             │
│                                       │ Redis        │             │
│                                       │ (Online      │             │
│                                       │  Status)     │             │
│                                       └──────┬───────┘             │
│                                              │                      │
│                                              ▼                      │
│                                       ┌──────────────┐             │
│                                       │ Push to      │             │
│                                       │ Subscribers  │             │
│                                       └──────────────┘             │
└─────────────────────────────────────────────────────────────────────┘

Presence Service Implementation

class PresenceService:
    """Track user online/offline status."""

    def __init__(self):
        self.redis = RedisCluster()
        self.ONLINE_TTL = 300       # 5 minutes — must heartbeat within this
        self.LAST_SEEN_TTL = 86400  # 24 hours for last_seen

    async def user_online(self, user_id: str):
        """Mark user as online."""
        pipe = self.redis.pipeline()

        # Set online status with TTL
        pipe.setex(f"presence:{user_id}", self.ONLINE_TTL, 'online')

        # Record last seen time
        pipe.setex(
            f"last_seen:{user_id}",
            self.LAST_SEEN_TTL,
            str(time.time())
        )

        # Notify subscribers (contacts who care about this user's status)
        subscribers = self.get_presence_subscribers(user_id)
        for sub_id in subscribers:
            pipe.publish(
                f"presence:{sub_id}",
                json.dumps({
                    'user_id': user_id,
                    'status': 'online',
                    'last_seen': time.time()
                })
            )

        pipe.execute()

    async def user_offline(self, user_id: str):
        """Mark user as offline."""
        pipe = self.redis.pipeline()

        # Remove online status
        pipe.delete(f"presence:{user_id}")

        # Update last seen
        pipe.setex(
            f"last_seen:{user_id}",
            self.LAST_SEEN_TTL,
            str(time.time())
        )

        # Notify subscribers
        subscribers = self.get_presence_subscribers(user_id)
        for sub_id in subscribers:
            pipe.publish(
                f"presence:{sub_id}",
                json.dumps({
                    'user_id': user_id,
                    'status': 'offline',
                    'last_seen': time.time()
                })
            )

        pipe.execute()

    async def get_presence(self, user_id: str) -> dict:
        """Get current presence status."""
        status = self.redis.get(f"presence:{user_id}")
        last_seen = self.redis.get(f"last_seen:{user_id}")

        return {
            'user_id': user_id,
            'status': 'online' if status else 'offline',
            'last_seen': float(last_seen) if last_seen else None
        }

Read Receipts System

class ReadReceiptService:
    """Handle message read status (sent → delivered → read)."""

    def __init__(self):
        self.cassandra = CassandraClient()
        self.redis = RedisCluster()

    async def mark_delivered(self, message_id: str, recipient_id: str):
        """Mark message as delivered (received by recipient's device)."""
        self.cassandra.execute("""
            INSERT INTO message_status (message_id, user_id, status, status_time)
            VALUES (%s, %s, 'delivered', toTimestamp(now()))
        """, (message_id, recipient_id))

        # Notify sender of delivery
        sender_id = self.get_message_sender(message_id)
        await self.notify_status(sender_id, message_id, 'delivered')

    async def mark_read(self, conversation_id: str, reader_id: str, last_sequence: int):
        """Mark all messages up to last_sequence as read."""
        # Get all unread messages in conversation up to sequence
        messages = self.cassandra.execute("""
            SELECT message_id FROM messages
            WHERE conversation_id = %s
            AND sequence <= %s
        """, (conversation_id, last_sequence))

        # Batch update status
        batch = []
        for msg in messages:
            batch.append((msg.message_id, reader_id, 'read'))

        # Batch insert for efficiency
        self.cassandra.execute("""
            INSERT INTO message_status (message_id, user_id, status, status_time)
            VALUES (%s, %s, 'read', toTimestamp(now()))
        """, batch)

        # Update unread count
        self.redis.hset(
            f"unread:{conversation_id}",
            reader_id,
            0
        )

        # Notify conversation participants
        participants = self.get_conversation_participants(conversation_id)
        for participant_id in participants:
            if participant_id != reader_id:
                await self.notify_read_receipt(
                    participant_id,
                    conversation_id,
                    reader_id,
                    last_sequence
                )

    async def get_message_status(self, message_id: str) -> dict:
        """Get delivery/read status for a message."""
        statuses = self.cassandra.execute("""
            SELECT user_id, status, status_time
            FROM message_status
            WHERE message_id = %s
        """, (message_id,))

        result = {
            'sent': True,
            'delivered_to': [],
            'read_by': []
        }

        for row in statuses:
            if row.status == 'delivered':
                result['delivered_to'].append({
                    'user_id': str(row.user_id),
                    'time': row.status_time
                })
            elif row.status == 'read':
                result['read_by'].append({
                    'user_id': str(row.user_id),
                    'time': row.status_time
                })

        return result

Group Messaging

class GroupMessagingService:
    """Handle group chat with fanout delivery."""

    def __init__(self):
        self.cassandra = CassandraClient()
        self.connection_manager = ConnectionManager()
        self.message_store = MessageStore()

    async def send_group_message(
        self,
        sender_id: str,
        group_id: str,
        content: str,
        content_type: str
    ) -> str:
        """Send message to a group."""
        # Step 1: Validate sender is in group
        if not await self.is_group_member(group_id, sender_id):
            raise UnauthorizedError("Not a group member")

        # Step 2: Generate message
        message_id = str(uuid.uuid4())
        sequence = await self.get_next_sequence(f"group:{group_id}")

        msg_record = {
            'message_id': message_id,
            'sender_id': sender_id,
            'conversation_id': group_id,
            'is_group': True,
            'content': content,
            'content_type': content_type,
            'sequence': sequence,
            'timestamp': time.time()
        }

        # Step 3: Persist
        await self.message_store.save(msg_record)

        # Step 4: Fanout to all group members
        members = await self.get_group_members(group_id)

        for member_id in members:
            if member_id == sender_id:
                continue  # Don't send to sender

            connection = self.connection_manager.get_connection(member_id)
            if connection:
                await connection.send_json({
                    'type': 'group_message',
                    'data': msg_record
                })
            else:
                # Queue + push notification
                await self.queue_offline_message(member_id, msg_record)

        return message_id

    async def get_group_members(self, group_id: str) -> List[str]:
        """Get all members of a group."""
        # Cache in Redis for frequent access
        cached = self.redis.smembers(f"group_members:{group_id}")
        if cached:
            return list(cached)

        # Fetch from database
        members = self.cassandra.execute("""
            SELECT user_id FROM group_members
            WHERE group_id = %s
        """, (group_id,))

        member_ids = [str(m.user_id) for m in members]

        # Cache for next time
        if member_ids:
            pipe = self.redis.pipeline()
            pipe.sadd(f"group_members:{group_id}", *member_ids)
            pipe.expire(f"group_members:{group_id}", 3600)
            pipe.execute()

        return member_ids

End-to-End Encryption (Signal Protocol)

class E2EEncryption:
    """Signal Protocol implementation for E2E encryption."""

    def __init__(self):
        self.key_store = KeyStore()

    async def register_keys(self, user_id: str):
        """Generate and upload identity key + signed pre-key."""
        # Generate identity key pair (long-term)
        identity_key = SignalProtocol.generate_identity_key_pair()

        # Generate signed pre-key (rotated periodically)
        signed_pre_key = SignalProtocol.generate_signed_pre_key(
            identity_key, key_id=1
        )

        # Generate one-time pre-keys (for initial key exchange)
        one_time_pre_keys = [
            SignalProtocol.generate_pre_key(identity_key, i)
            for i in range(100)
        ]

        # Upload to server
        await self.key_store.upload_keys(
            user_id=user_id,
            identity_key=identity_key.public,
            signed_pre_key=signed_pre_key,
            one_time_pre_keys=[k.public for k in one_time_pre_keys]
        )

    async def establish_session(self, sender_id: str, recipient_id: str):
        """Establish encrypted session with recipient."""
        # Fetch recipient's keys from server
        recipient_keys = await self.key_store.get_keys(recipient_id)

        # Perform X3DH key agreement
        session = SignalProtocol.create_session(
            our_identity_key=await self.key_store.get_identity(sender_id),
            our_signed_pre_key=await self.key_store.get_signed_pre_key(sender_id),
            their_identity_key=recipient_keys['identity_key'],
            their_signed_pre_key=recipient_keys['signed_pre_key'],
            their_one_time_pre_key=recipient_keys['one_time_pre_keys'][0]
        )

        return session

    def encrypt_message(self, session, plaintext: str) -> bytes:
        """Encrypt message using Double Ratchet algorithm."""
        return SignalProtocol.encrypt(session, plaintext.encode('utf-8'))

    def decrypt_message(self, session, ciphertext: bytes) -> str:
        """Decrypt received message."""
        return SignalProtocol.decrypt(session, ciphertext).decode('utf-8')

Encryption Flow

┌─────────────────────────────────────────────────────────────────────┐
│                    E2E ENCRYPTION FLOW                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Alice                           Server                          Bob │
│    │                               │                               │
│    │─── Upload Identity Key ──────▶│                               │
│    │─── Upload Signed Pre-Key ────▶│                               │
│    │─── Upload One-Time Pre-Keys ─▶│                               │
│    │                               │                               │
│    │                               │◀─── Request Bob's Keys ───────│
│    │                               │──── Return Bob's Keys ───────▶│
│    │                               │                               │
│    │  X3DH Key Agreement          │  X3DH Key Agreement           │
│    │  (generates shared secret)    │  (generates shared secret)     │
│    │                               │                               │
│    │  Double Ratchet              │  Double Ratchet               │
│    │  (derives per-message keys)  │  (derives per-message keys)   │
│    │                               │                               │
│    │──── Encrypted Message ──────▶│──── Forward (encrypted) ─────▶│
│    │    (server can't read)       │    (server can't read)        │
│    │                               │                               │
│    │                               │◀──── Encrypted Reply ─────────│
│    │◀─── Encrypted Reply ─────────│                               │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Typing Indicators

class TypingIndicator:
    """Handle typing indicators with debouncing."""

    def __init__(self):
        self.redis = RedisCluster()
        self.connection_manager = ConnectionManager()
        self.TYPING_TTL = 10  # Auto-expire after 10 seconds

    async def start_typing(self, user_id: str, conversation_id: str):
        """User started typing."""
        # Store in Redis with short TTL
        self.redis.setex(
            f"typing:{conversation_id}:{user_id}",
            self.TYPING_TTL,
            '1'
        )

        # Notify other participants
        participants = self.get_participants(conversation_id)
        for participant_id in participants:
            if participant_id != user_id:
                connection = self.connection_manager.get_connection(
                    participant_id
                )
                if connection:
                    await connection.send_json({
                        'type': 'typing',
                        'conversation_id': conversation_id,
                        'user_id': user_id,
                        'is_typing': True
                    })

    async def stop_typing(self, user_id: str, conversation_id: str):
        """User stopped typing."""
        self.redis.delete(f"typing:{conversation_id}:{user_id}")

        # Notify to hide typing indicator
        participants = self.get_participants(conversation_id)
        for participant_id in participants:
            if participant_id != user_id:
                connection = self.connection_manager.get_connection(
                    participant_id
                )
                if connection:
                    await connection.send_json({
                        'type': 'typing',
                        'conversation_id': conversation_id,
                        'user_id': user_id,
                        'is_typing': False
                    })

System Architecture Summary

┌─────────────────────────────────────────────────────────────────────────────┐
│                          WHATSAPP ARCHITECTURE                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌──────────┐                                                              │
│  │  Mobile  │──┐                                                           │
│  │  Client  │  │    ┌──────────────┐                                      │
│  └──────────┘  ├───▶│  Load        │    ┌──────────────┐                  │
│                │    │  Balancer    │───▶│  Connection  │  (WebSocket       │
│  ┌──────────┐  │    └──────────────┘    │  Servers     │   Gateway)        │
│  │  Web     │──┘                        └──────┬───────┘                  │
│  │  Client  │                                  │                           │
│  └──────────┘                    ┌─────────────┼─────────────┐            │
│                                  │             │             │            │
│                                  ▼             ▼             ▼            │
│                           ┌───────────┐ ┌───────────┐ ┌───────────┐     │
│                           │   Chat    │ │ Presence  │ │  Read     │     │
│                           │  Service  │ │  Service  │ │  Receipt  │     │
│                           └─────┬─────┘ └─────┬─────┘ │  Service  │     │
│                                 │             │       └─────┬─────┘     │
│                                 ▼             ▼             ▼           │
│                          ┌───────────┐ ┌───────────┐ ┌───────────┐     │
│                          │  Kafka    │ │  Redis    │ │Cassandra  │     │
│                          │ (Message  │ │ (Online   │ │(Messages  │     │
│                          │  Queue)   │ │  Status)  │ │ & Status) │     │
│                          └─────┬─────┘ └───────────┘ └───────────┘     │
│                                │                                        │
│                          ┌─────▼─────┐                                  │
│                          │  Offline  │                                  │
│                          │  Workers  │                                  │
│                          └─────┬─────┘                                  │
│                                │                                        │
│                          ┌─────▼─────┐                                  │
│                          │ Push      │                                  │
│                          │ (APNS/FCM)│                                  │
│                          └───────────┘                                  │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    E2E ENCRYPTION LAYER                         │   │
│  │  - Signal Protocol (X3DH + Double Ratchet)                      │   │
│  │  - Server never sees plaintext messages                         │   │
│  │  - Keys managed client-side, uploaded to server for exchange     │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

Practice Problems

0/3solved
Design WhatsApp / Chat System (Design WhatsApp) System

Design a scalable WhatsApp / Chat System (Design WhatsApp) system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
WhatsApp / Chat System (Design WhatsApp) Scaling

How would you scale WhatsApp / Chat System (Design WhatsApp) to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
WhatsApp / Chat System (Design WhatsApp) Failure Modes

Analyze potential failure modes for WhatsApp / Chat System (Design WhatsApp) and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. Why does WhatsApp use WebSocket instead of HTTP polling for real-time messaging?

Question 1 options

2. How does the system handle message ordering in a distributed environment?

Question 2 options

3. What happens when a user is offline and receives a message?

Question 3 options

4. How does WhatsApp ensure end-to-end encryption while still supporting multi-device sync?

Question 4 options

5. Why is Cassandra used for message storage instead of PostgreSQL?

Question 5 options

6. How does the heartbeat mechanism detect disconnections?

Question 6 options

7. What is the purpose of the deduplication mechanism in message processing?

Question 7 options

Flashcards

Question

What is the message delivery flow in WhatsApp?

Answer

Sender → Connection Server (WebSocket) → Chat Service → Validate + Generate Sequence → Persist to Cassandra → Route to Recipient: if online → deliver via WebSocket; if offline → queue in Kafka + send push notification via APNS/FCM

Question

How does message ordering work across distributed servers?

Answer

Each conversation has a monotonically increasing sequence number generated server-side using Redis atomic INCR. This guarantees strict per-conversation ordering regardless of which server processes the message or network timing.

Question

What is the role of Kafka in WhatsApp's architecture?

Answer

Kafka serves as the message queue for offline message delivery. When a recipient is offline, messages are queued in Kafka and delivered when they come online. Kafka provides at-least-once delivery semantics, durability, and parallelism via consumer groups.

Question

How does WhatsApp implement read receipts?

Answer

Three states: Sent (server received) → Delivered (recipient's device received, confirmed via WebSocket ack) → Read (recipient opened the chat, sends read_ack with last_read_sequence). Each state transition is stored in message_status table and notified to sender.

Question

What is the Signal Protocol and why does WhatsApp use it?

Answer

Signal Protocol provides end-to-end encryption using X3DH key agreement + Double Ratchet algorithm. Messages are encrypted on sender's device and only decrypted on recipient's device. WhatsApp uses it so the server never sees plaintext message content.

Question

How does presence detection work?

Answer

Each connected client maintains a Redis key with 5-min TTL. The client sends heartbeat messages every 60 seconds to refresh the TTL. If the TTL expires (no heartbeat), the user is marked offline. Other users subscribe to presence changes via Redis Pub/Sub.

Question

How does group messaging handle fanout delivery?

Answer

When a message is sent to a group, the Chat Service fetches all group members, then for each member: if online → deliver via their WebSocket connection; if offline → queue in Kafka + push notification. Group membership is cached in Redis for fast access.

Question

Why use optimistic UI updates in chat?

Answer

When a user sends a message, it appears in the UI immediately (status: 'sending') before server confirmation. This provides instant feedback. On acknowledgment, the status updates to 'sent'/'delivered'. On failure, the user sees a retry option. Improves perceived performance.

Question

How does message deduplication prevent duplicates?

Answer

Each client generates a unique client_msg_id before sending. The server uses Redis SETNX (atomic set-if-not-exists) with this ID. If SETNX returns false, the message was already processed (duplicate from retry) and is skipped. TTL of 24h prevents Redis key accumulation.

Revision Notes

Key Takeaways

  • 1.WebSocket is essential for real-time bidirectional communication — HTTP polling wastes resources
  • 2.Server-side sequence numbers (Redis INCR) solve the distributed ordering problem cleanly
  • 3.Offline support requires three components: persistent storage (Cassandra), message queue (Kafka), and push notifications (APNS/FCM)
  • 4.Presence detection via Redis TTL + heartbeat is elegant and scalable — no explicit disconnect needed
  • 5.E2E encryption with Signal Protocol means the server is a dumb relay — it stores and forwards encrypted blobs
  • 6.Group messaging is fanout: fetch members, deliver to each individually (online/offline routing per member)
  • 7.Deduplication via client_msg_id + Redis SETNX prevents duplicate processing from retries

Interview Tips

  • Start with the protocol choice: explain why WebSocket over HTTP polling (persistent connection, bidirectional, lower latency)
  • Walk through the message flow end-to-end: sender → server → persistence → routing → recipient
  • Emphasize the offline story: Cassandra for persistence, Kafka for queueing, APNS/FCM for push
  • Discuss message ordering with sequence numbers — interviewers expect this detail
  • Be ready to explain read receipts: the three states and how they're tracked
  • Mention E2E encryption but don't go too deep — acknowledge Signal Protocol, explain that server can't read messages
  • For group messaging, discuss fanout strategy and how it differs from 1:1
  • Address scalability: how does this handle 500M concurrent WebSocket connections? (Connection servers, load balancing, sharding by user_id)
  • Discuss failure modes: what happens when WebSocket drops? When Kafka is down?

Cheat Sheet

WhatsApp System Design Cheat Sheet

Scale Numbers

  • 2B MAU, 1.5B DAU
  • 100B messages/day → ~1.16M msgs/sec
  • Peak: 50M msgs/sec
  • 500M concurrent connections
  • Avg message: 100 bytes text, 100KB media (10%)

Core Architecture

Client → Load Balancer → Connection Servers (WebSocket)
    ├── Chat Service → Cassandra (persist) + Kafka (offline queue)
    ├── Presence Service → Redis (online status with TTL)
    ├── Read Receipt Service → Cassandra (message_status)
    └── Notification Service → APNS/FCM (push)

Message Flow

  1. Client sends via WebSocket with client_msg_id
  2. Server validates sender, generates sequence number (Redis INCR)
  3. Persist to Cassandra (partition by conversation_id)
  4. Route to recipient:
    • Online → deliver via WebSocket connection
    • Offline → queue in Kafka + push notification
  5. Delivery ack → sender sees double checkmark
  6. Read ack → sender sees blue checkmark

Message Ordering

  • Server-side sequence numbers per conversation
  • Redis INCR guarantees atomic, monotonic increment
  • Client messages include client_msg_id for deduplication
  • Ordering: (conversation_id, sequence) is globally unique

Presence System

  • Redis key: presence:{user_id} with 5-min TTL
  • Heartbeat every 60 seconds refreshes TTL
  • TTL expiry = user marked offline
  • Redis Pub/Sub notifies contacts of status changes
  • last_seen timestamp stored for offline users

Read Receipts

  • Three states: Sent → Delivered → Read
  • Sent: server acknowledged (implicit on receive)
  • Delivered: recipient device ack (WebSocket delivery_ack)
  • Read: recipient opened chat (read_ack with last_read_sequence)
  • Batch update for efficiency (mark all up to sequence as read)

End-to-End Encryption

  • Signal Protocol: X3DH key agreement + Double Ratchet
  • Each device has own identity key pair
  • Server stores public keys for key exchange
  • Messages encrypted client-side, server never sees plaintext
  • Multi-device: separate encryption per device

Group Messaging

  • Group membership cached in Redis (set per group_id)
  • Fanout: for each member → online: WebSocket, offline: Kafka + push
  • Sequence numbers per group (not per conversation)
  • Admin controls: add/remove members, change settings

Key Decisions

Decision Choice Why
Transport WebSocket Persistent bidirectional, low latency
Message Storage Cassandra High write throughput, partition by conversation
Offline Queue Kafka Durable, at-least-once, parallel consumers
Online Status Redis Sub-ms reads, TTL for auto-expiry
Encryption Signal Protocol Industry standard E2E, multi-device support
Push Notifications APNS/FCM Native platform push for offline users

Failure Modes

  • WebSocket drops → heartbeat detects, mark offline, queue messages
  • Kafka down → messages persisted in Cassandra, retry delivery later
  • Redis down → rebuild presence from Cassandra, degraded but functional
  • Cassandra down → messages queue in Kafka, deliver when recovered
  • Push service down → messages still in queue, retry push later