Design a Chat Application

Difficulty: Advanced

Question

Design a real-time chat application like WhatsApp or Slack. Support 1:1 chats, group chats, and online presence.

Answer

Core components: 1. WebSocket Gateway: Persistent connections for real-time messaging 2. Chat Service: Message routing, group management 3. Message Store: Persistent message storage (Cassandra or PostgreSQL) 4. Presence Service: Online/offline/typing status 5. Notification Service: Push notifications for offline users 6. Media Service: File/image uploads to S3

Key challenges: - Maintaining millions of concurrent WebSocket connections - Message ordering and delivery guarantees - Offline message delivery - Group chat fan-out (sending to N members) - End-to-end encryption

Code examples

WebSocket Message Flow

// Chat Gateway Server
const connections = new Map(); // userId -> WebSocket

wss.on('connection', (ws, req) => {
  const userId = authenticateFromToken(req);
  connections.set(userId, ws);
  presenceService.setOnline(userId);

  ws.on('message', async (data) => {
    const msg = JSON.parse(data);

    switch (msg.type) {
      case 'SEND_MESSAGE': {
        // 1. Store message in DB
        const saved = await messageService.save({
          id: generateId(),
          senderId: userId,
          chatId: msg.chatId,
          content: msg.content,
          timestamp: Date.now(),
          status: 'SENT'
        });

        // 2. Get recipients
        const members = await chatService.getMembers(msg.chatId);

        // 3. Deliver to online users
        for (const memberId of members) {
          const recipientWs = connections.get(memberId);
          if (recipientWs?.readyState === WebSocket.OPEN) {
            recipientWs.send(JSON.stringify({
              type: 'NEW_MESSAGE',
              message: saved
            }));
          } else {
            // 4. Queue for offline delivery + push notification
            await notificationQueue.push(memberId, saved);
          }
        }
        break;
      }

      case 'TYPING':
        broadcastToChat(msg.chatId, userId, { type: 'TYPING', userId });
        break;

      case 'READ_RECEIPT':
        await messageService.markRead(msg.messageId, userId);
        break;
    }
  });

  ws.on('close', () => {
    connections.delete(userId);
    presenceService.setOffline(userId);
  });
});

The gateway maintains WebSocket connections and routes messages. Online users get instant delivery; offline users receive push notifications and sync messages when they reconnect.

Message Storage Schema

-- Messages table (optimized for chat history queries)
CREATE TABLE messages (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  chat_id UUID NOT NULL,
  sender_id UUID NOT NULL,
  content TEXT,
  media_url VARCHAR(500),
  message_type VARCHAR(20) DEFAULT 'TEXT',
  status VARCHAR(20) DEFAULT 'SENT',
  created_at TIMESTAMP DEFAULT NOW(),
  edited_at TIMESTAMP
);

-- Partition by chat_id for fast retrieval
CREATE INDEX idx_messages_chat_time
  ON messages(chat_id, created_at DESC);

-- Chat members
CREATE TABLE chat_members (
  chat_id UUID REFERENCES chats(id),
  user_id UUID REFERENCES users(id),
  role VARCHAR(20) DEFAULT 'MEMBER',
  last_read_at TIMESTAMP,
  joined_at TIMESTAMP DEFAULT NOW(),
  PRIMARY KEY (chat_id, user_id)
);

-- Unread count query
SELECT COUNT(*) as unread
FROM messages m
WHERE m.chat_id = $1
  AND m.created_at > (
    SELECT last_read_at FROM chat_members
    WHERE chat_id = $1 AND user_id = $2
  );

Messages are indexed by chat_id and timestamp for fast history retrieval. Unread counts use the last_read_at timestamp to avoid storing per-message read status for every user.

Presence System with Redis

// Presence service using Redis
class PresenceService {
  async setOnline(userId) {
    // Set user as online with 30s TTL
    await redis.set(`presence:${userId}`, 'online', 'EX', 30);
    // Heartbeat extends TTL every 10s from client
  }

  async setOffline(userId) {
    await redis.del(`presence:${userId}`);
    await redis.set(`last_seen:${userId}`, Date.now());
  }

  async getStatus(userId) {
    const online = await redis.get(`presence:${userId}`);
    if (online) return { status: 'online' };

    const lastSeen = await redis.get(`last_seen:${userId}`);
    return { status: 'offline', lastSeen };
  }

  // Batch check for contact list
  async getBulkStatus(userIds) {
    const pipeline = redis.pipeline();
    userIds.forEach(id => pipeline.get(`presence:${id}`));
    const results = await pipeline.exec();
    return userIds.map((id, i) => ({
      userId: id,
      online: results[i][1] === 'online'
    }));
  }
}

Redis with TTL-based keys provides an efficient presence system. Heartbeats keep the TTL alive for online users. Batch queries avoid N+1 lookups for contact lists.

Key points

Concepts covered

WebSocket, Message Queue, Presence System, Message Storage, Fan-out