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

Diwali family WhatsApp group chaos

Fifty-three relatives. One group. Zero punctuation from Nani.

Chat system architecture

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

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.