SYSTEM DESIGN:Lesson 21: How WhatsApp-Style Chat Works
Mastering lesson 21: how whatsapp-style chat works concepts and implementation.
Diwali night, family WhatsApp group
Fifty-three relatives in "Sharma Family πͺ". Mummy sends a photo of diyas. Chacha forwards a Good Morning meme from 2019. Your cousin in Toronto replies at 3 AM his time. Nani types "happy diwali beta" without punctuation. Messages arrive out of order sometimes. Blue ticks appear. Someone sends a voice note. The group never sleeps.
That is a chat system. Not email. Not a news feed. Real-time (or near-real-time) messaging with delivery states, 1:1 and group conversations, and the expectation that if you sent it, the other person will see it β even if they were offline.
This chapter covers the core: message flow, WebSocket vs polling, schema, delivery states. No gateway clusters, no presence at scale. Lesson 22.

Diwali family WhatsApp group chaos
Fifty-three relatives. One group. Zero punctuation from Nani.
Chat system architecture
Blue ticks are a distributed systems problem wearing a UX costume.
Requirements β functional and non-functional
Functional
- 1:1 chat β two users exchange messages in a private conversation
- Group chat β N users in one conversation (family group, office team)
- Message history β scroll up to see older messages
- Delivery states β sent β, delivered ββ, read ββ (blue)
- Offline support β messages queue and deliver when user comes back
- Optional: media, typing indicators, reply-to (mention, do not over-build)
Non-functional
- Delivery latency β < 100 ms for online recipients; feels instant
- Message ordering β mostly chronological per conversation; perfect global order not required
- Durability β once server ACKs send, message must not be lost
- Scale hint: write-heavy during festivals; 1:1 is symmetric, groups fan out
Clarify: WhatsApp-scale or startup MVP? I commit to text messages, 1:1 + group, three delivery states. Skip E2E encryption implementation unless asked β mention it exists in production.
Clarifying with the interviewer
You: "1:1 and group chat both in scope?"
-> Interviewer: "Yes, group up to 256 members."
You: "Real-time delivery β WebSocket acceptable?"
-> Interviewer: "Yes."
You: "Do we need read receipts and delivery ticks?"
-> Interviewer: "Yes, like WhatsApp."
You: "Offline messages β store and deliver on reconnect?"
-> Interviewer: "Yes."
You: "I will cover send/store/deliver flow, schema, WebSocket
transport, and delivery state machine. Scale in follow-up."
1:1 vs group chat
1:1 conversation:
- Exactly two participants
- Message sent once, delivered to one recipient
- Simple routing: recipient_id known at send time
Group conversation:
- N participants (cap at 256 for MVP β WhatsApp limit)
- Message sent once, fan-out to N-1 other members
- Each member has own delivery/read state per message
Group chat is a mini news feed fan-out inside one room. Same push pattern, smaller N. Diwali family group with 53 members is manageable synchronously; 256-member office group needs async fan-out.
WebSocket vs long polling
Client needs server-initiated delivery β HTTP request/response alone cannot push.
Long polling
Client: GET /messages/poll?since=msg_123
Server: holds connection open up to 30 sec
if new message β return immediately
else β return empty, client reconnects
Pros: works through corporate firewalls, no special protocol
Cons: high connection churn, latency spikes, wasteful on mobile battery
WebSocket (recommended)
Client: WS connect wss://chat.schoolabe.com/ws?token=...
Server: persistent bidirectional channel
Client sends: { "type": "send", "conversation_id": "...", "body": "..." }
Server pushes: { "type": "message", "payload": { ... } }
Pros: low latency, single connection, efficient for mobile
Cons: load balancer sticky sessions or connection registry at scale (Lesson 22)
Interview pick: WebSocket for real-time chat. Mention long polling as fallback for restrictive networks. Nobody designs WhatsApp on long polling in 2026.
Message flow: send β store β deliver
The happy path for 1:1 chat:
1. Sender types message, taps Send
2. Client sends via WebSocket (or POST if WS down)
3. Chat server validates, assigns message_id, persists to DB
4. Server ACKs sender β "sent" β
5. Server looks up recipient's active WebSocket connection
6. If online: push message β recipient ACKs β "delivered" ββ to sender
7. Recipient opens chat β "read" ββ blue to sender
8. If offline: message stays in DB; push notification via notification service
Chat system architecture
Critical rule: persist before ACK. If server crashes after ACK but before DB write, message is lost. Write to DB (or durable queue) first, then ACK sender.
Delivery state machine
ββββββββββββ
β sending β (client optimistic UI)
ββββββ¬ββββββ
β server ACK
ββββββΌββββββ
β sent β β
ββββββ¬ββββββ
β recipient device ACK
ββββββΌββββββ
β deliveredβ ββ
ββββββ¬ββββββ
β recipient opens chat / read_receipt
ββββββΌββββββ
β read β ββ blue
ββββββββββββ
Each state transition is a separate event β sender's UI updates via WebSocket push.
def on_message_received_by_device(message_id, recipient_id):
db.update_delivery_state(message_id, recipient_id, "delivered")
notify_sender(message_id, state="delivered")
def on_message_read(message_id, recipient_id):
db.update_delivery_state(message_id, recipient_id, "read")
notify_sender(message_id, state="read")
Group chat: track delivery state per participant per message. 53-member group = 53 rows in message_delivery table for one message.
Database schema
Conversations
CREATE TABLE conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(10) NOT NULL CHECK (type IN ('direct', 'group')),
title VARCHAR(100), -- group name; NULL for 1:1
created_at TIMESTAMP DEFAULT NOW()
);
Participants
CREATE TABLE participants (
conversation_id UUID NOT NULL REFERENCES conversations(id),
user_id BIGINT NOT NULL REFERENCES users(id),
joined_at TIMESTAMP DEFAULT NOW(),
last_read_at TIMESTAMP, -- for unread badge
PRIMARY KEY (conversation_id, user_id)
);
CREATE INDEX idx_participants_user ON participants(user_id);
Index on user_id β "list all conversations for this user."
Messages
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
conversation_id UUID NOT NULL REFERENCES conversations(id),
sender_id BIGINT NOT NULL REFERENCES users(id),
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_messages_conv_created ON messages(conversation_id, created_at DESC);
Message delivery (per-recipient state)
CREATE TABLE message_delivery (
message_id BIGINT NOT NULL REFERENCES messages(id),
recipient_id BIGINT NOT NULL REFERENCES users(id),
state VARCHAR(20) NOT NULL DEFAULT 'sent'
CHECK (state IN ('sent', 'delivered', 'read')),
updated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (message_id, recipient_id)
);
For 1:1 chat, sender is excluded from message_delivery β only the other participant gets a row.
API design
Send message (WebSocket frame)
{
"type": "send_message",
"conversation_id": "a1b2c3d4-...",
"client_msg_id": "local-uuid-for-dedup",
"body": "Happy Diwali! πͺ"
}
Server response:
{
"type": "message_ack",
"client_msg_id": "local-uuid-for-dedup",
"message_id": 84729163,
"state": "sent",
"created_at": "2026-11-01T18:30:00Z"
}
client_msg_id lets the client deduplicate retries β tap Send twice on slow network should not create two messages.
Get message history (HTTP)
GET /api/v1/conversations/{id}/messages?limit=50&before=84729100
Authorization: Bearer <token>
Cursor-based pagination on created_at or message_id. User scrolls up β fetch older batch.
List conversations
GET /api/v1/conversations
Returns conversations sorted by last_message_at β denormalized column updated on each new message.
Group chat send flow
def send_group_message(conversation_id, sender_id, body):
msg = db.insert_message(conversation_id, sender_id, body)
recipients = db.get_participants(conversation_id) - {sender_id}
for rid in recipients:
db.insert_delivery(msg.id, rid, state="sent")
if is_online(rid):
ws_push(rid, msg)
else:
notification_service.send_push(rid, preview=body[:50])
return msg
Push notification for offline users ties into the notification system β separate service, separate chapter. Link: Notification System.
Offline sync (brief)
On reconnect: client sends last_synced_message_id per conversation β server returns gap β client merges local SQLite cache β batch delivery ACKs. Lesson 22 covers sync at 100M DAU.
Preview: what Lesson 22 adds
Core design breaks when:
- 100M DAU, each holding a WebSocket open
- Gateway needs to route message to correct server holding recipient's connection
- Group with 256 members during festival = fan-out storm
- "Exactly once" delivery under network partitions
Continue here: Chat System at Scale β Gateways, Presence, and Partitioning.