Mari desain sistem chat real-time seperti WhatsApp atau Slack. Ini contoh system design yang menggabungkan banyak konsep yang sudah kita pelajari.
Step 1: Requirements
Functional:
- Kirim pesan 1-on-1 dan group chat
- Real-time delivery (pesan langsung muncul)
- Online/offline status
- Read receipts (tanda sudah dibaca)
- Message history (bisa scroll ke atas)
Non-functional:
- Latency < 100ms untuk delivery
- High availability
- Message ordering yang benar
- Support offline (pesan terkirim saat online kembali)
Step 2: Estimasi
500 juta user aktif, rata-rata 50 pesan/hari
Total: 25 miliar pesan/hari
= 290.000 pesan/detik
Storage per pesan: ~200 bytes
Per hari: 25B × 200 = 5 TB/hari
Per tahun: 1.8 PB (petabyte!)
Step 3: Arsitektur High-Level
┌─────────┐ WebSocket ┌─────────────┐
│ Client │ ◄───────────────► │ Chat Server │
│ (App) │ │ (Stateful) │
└─────────┘ └──────┬──────┘
│
┌──────┴──────┐
│ Message │
│ Queue │
└──────┬──────┘
│
┌──────────────┬───────┴────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌──────┴──────┐
│ Message │ │ Presence │ │ Notification│
│ Store │ │ Service │ │ Service │
└───────────┘ └───────────┘ └─────────────┘
Step 4: Real-time Messaging (WebSocket)
// Server: manage WebSocket connections
const connections = new Map(); // userId → WebSocket
wss.on("connection", (ws, req) => {
const userId = authenticate(req);
connections.set(userId, ws);
ws.on("message", async (data) => {
const msg = JSON.parse(data);
// Simpan ke database
const saved = await saveMessage(msg);
// Kirim ke penerima jika online
const recipientWs = connections.get(msg.recipientId);
if (recipientWs) {
recipientWs.send(JSON.stringify(saved));
} else {
// Offline → push notification
await pushService.notify(msg.recipientId, saved);
}
// Kirim ack ke pengirim
ws.send(JSON.stringify({ type: "ack", messageId: saved.id }));
});
ws.on("close", () => {
connections.delete(userId);
updatePresence(userId, "offline");
});
});
Step 5: Message Storage
Pilihan database:
- Cassandra/ScyllaDB → write-heavy, partitioned by conversation
- PostgreSQL → kuat di query, cocok untuk fitur search
Schema:
messages:
message_id: UUID (time-based, e.g. Snowflake ID)
conversation_id: UUID
sender_id: BIGINT
content: TEXT (encrypted)
type: ENUM(text, image, file)
created_at: TIMESTAMP
read_by: JSON
Partition key: conversation_id
Sort key: created_at (message ordering)
Step 6: Group Chat
Group:
group_id, name, members[], admin[]
Kirim pesan ke group:
1. Simpan pesan 1x di database
2. Fan-out: kirim ke semua member yang online via WebSocket
3. Member offline → simpan di "undelivered" queue
Optimasi fan-out:
- Small group (< 100): fan-out on write (kirim langsung)
- Large group (> 100): fan-out on read (lazy loading)
Step 7: Presence (Online/Offline Status)
// Heartbeat approach:
// Client kirim ping setiap 30 detik
// Jika tidak ada ping dalam 60 detik → anggap offline
setInterval(() => {
redis.set("presence:" + userId, "online", "EX", 60);
}, 30000);
// Cek status user:
async function isOnline(userId) {
return await redis.exists("presence:" + userId);
}
Step 8: End-to-End Encryption
Setiap user punya key pair (public + private):
1. Sender encrypt pesan dengan public key penerima
2. Hanya penerima yang bisa decrypt dengan private key-nya
3. Server hanya menyimpan ciphertext — tidak bisa baca pesan!
Scaling:
- Horizontal scale WebSocket servers, gunakan Redis Pub/Sub untuk cross-server messaging
- Shard message database by conversation_id
- CDN untuk media (gambar, video, file)
- Message queue untuk push notification dan async tasks
System design chat app menggabungkan: WebSocket, database scaling, caching, message queue, CDN, dan real-time architecture.
🎭 Analogi sehari-hari: Chat = walkie-talkie zaman dulu. Tekan tombol kirim suara langsung ke channel — semua yang tune ke channel itu denger. WebSocket = "channel selalu terbuka", beda dari HTTP yang "tanya-jawab terus tutup". Tanpa WebSocket, client harus polling tiap detik = boros battery + traffic.
💡 Mengapa WebSocket bukan polling? HTTP polling = tanya server tiap 1 detik "ada pesan baru?" — 99% jawab "tidak" = waste. WebSocket = koneksi tetap terbuka, server push pas ada pesan. Beda urutan magnitudo: polling 86400 req/hari per user, WebSocket 0 (kecuali ada event).
⚠️ Jebakan chat system klasik:
- WebSocket sticky to server instance — server mati = koneksi putus. Wajib reconnect logic + state di Redis
- Message ordering — pesan B sampai sebelum A karena network. Pakai message ID monoton (Snowflake/UUID v7)
- Fan-out group besar — 1jt member group, kirim 1 pesan = 1jt push. Pakai fan-out on read
- Offline delivery = wajib persist message + push notification fallback
- No encryption = chat content bocor. E2E encryption kalau privasi matter
🎯 Komponen kritis:
- WebSocket gateway — long-lived connection per user
- Redis Pub/Sub — cross-server message routing
- Message store — Cassandra/ScyllaDB (write-heavy) atau partitioned PostgreSQL
- Presence service — Redis dengan TTL heartbeat
- Push notification — fallback untuk offline user (FCM/APNs)
- CDN — gambar/video/voice note dipisah dari chat server
TL;DR: Chat app = WebSocket (real-time) + message store (Cassandra) + Redis Pub/Sub (cross-server) + push notif (offline). Order kritis (Snowflake ID). E2E encryption optional. Group besar pakai fan-out on read.