Difficulty: Advanced
Design a real-time chat application like WhatsApp or Slack. Support 1:1 chats, group chats, and online presence.
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
// 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.
-- 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 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.
WebSocket, Message Queue, Presence System, Message Storage, Fan-out