Skip to content

02. WhatsApp / Messenger — Low-Level Design

~18 min read · Part 3 of 4 (Overview → HLD → LLD → Q&A)

Data models

CREATE TABLE message (
    conv_id       BIGINT NOT NULL,
    seq           BIGINT NOT NULL,        -- monotonic per conversation → ordering
    server_msg_id BIGINT NOT NULL,        -- global unique
    sender_id     BIGINT NOT NULL,
    client_msg_id UUID NOT NULL,          -- sender-generated → dedup
    body          BYTEA,                  -- ciphertext for E2E
    created_at    TIMESTAMP NOT NULL,
    PRIMARY KEY (conv_id, seq)
);                                         -- sharded by conv_id
CREATE UNIQUE INDEX ux_dedup ON message (conv_id, client_msg_id);

-- Inbox queue (per user):  inbox:{user_id} = ordered (conv_id, seq) entries undelivered
-- Session registry (Redis): sess:{user_id} = { gateway_node, conn_id, expires_at }
-- Receipts:  receipt(conv_id, seq, user_id, state, ts)

Two keys carry the guarantees. PRIMARY KEY (conv_id, seq) makes ordering a property of storage — reading a conversation in seq order is the correct order. The unique index on (conv_id, client_msg_id) makes dedup an insert conflict: a retransmitted message fails the unique constraint, so it's stored once no matter how many times Alice's client retries.

Component internals

Message service — persist-first, dedup, sequence

def handle_send(sender, msg):
    seq = sequencer.next(msg.conv_id)                  # monotonic per conversation
    sid = snowflake()
    try:
        store.insert(msg.conv_id, seq, sid, sender.id, msg.client_msg_id, msg.body)
    except UniqueViolation:                            # dedup: already stored
        existing = store.get_by_client_id(msg.conv_id, msg.client_msg_id)
        return Ack(existing.server_msg_id, "sent")     # idempotent ack
    ack(sender, Ack(sid, "sent"))                      # durable BEFORE delivery
    deliver_or_enqueue(msg.conv_id, seq, sid)
    return

The ordering is deliberate: persist and ack before attempting delivery. If delivery fails, the message is safe and will redeliver; if the ack is lost, the retransmit dedups. Alice never loses a message and never sees it twice.

Delivery router — online vs offline

def deliver_or_enqueue(conv_id, seq, sid):
    for recipient in members(conv_id) - {sender}:
        sess = session_registry.get(recipient)
        if sess and gateway_alive(sess.node):
            forward(sess.node, sess.conn_id, DELIVER(sid, ...))   # push now
        else:
            inbox.enqueue(recipient, conv_id, seq)                # offline → queue

For Bob offline, there's no live session, so the message enqueues. This is the same code path whether Bob is offline, mid-reconnect, or on a dead node — persist-first means enqueue is always a safe fallback.

Reconnect drain — ordered catch-up

def on_reconnect(user, last_seq_by_conv):
    session_registry.put(user, this_node, conn_id, ttl=30s)
    for conv_id, last_seq in last_seq_by_conv.items():
        for m in store.range(conv_id, after=last_seq):   # in seq order
            push(user, DELIVER(m.server_msg_id, ...))
    inbox.clear_delivered(user)

Bob sends the last seq he has per conversation; the server streams everything after it in order. Alice's "running late" arrives in its correct sequence slot even though it was sent while Bob was offline.

Core algorithm — per-conversation ordering under concurrency

sequencer.next(conv_id):
    # sequence lives on the conv_id's shard, so one writer per conversation
    return atomic_increment(seq_counter[conv_id])

client render:
    buffer messages, sort by seq within conv_id, render contiguously
    if a gap in seq (missing seq N): request retransmit of N before rendering past it

Ordering is per conversation, not global — two different conversations need no coordination. Because a conversation is pinned to one shard, its sequence is a simple atomic counter; the client detects gaps (a missing seq) and requests the missing message before rendering later ones, so order is never violated even if packets arrive scrambled.

Sequence diagram — offline delivery + receipts

Alice     A-gateway   Msg svc     Store     Bob-inbox    Bob        B-gateway
 │ SEND     │            │           │          │         (offline)     │
 ├──────────▶│ persist ──▶│ insert ──▶│          │           │           │
 │◀── ACK sent ──────────┤           │          │           │           │
 │           │ Bob offline?│ yes ─────┼─ enqueue▶│           │           │
 │           │            │           │          │  reconnect (last_seq) │
 │           │            │◀──────────┼──────────┼───────────┼───────────┤
 │           │            │ stream after last_seq (in order) ▶│ store+ACK │
 │◀─ RECEIPT delivered ───┤◀──────────┼──────────┼───────────┤           │

Concurrency and edge cases

  • Retransmit storm: the unique (conv_id, client_msg_id) index makes every duplicate a no-op insert; the server re-acks idempotently.
  • Two devices, same user: each device is its own session; the message fans out to all of a user's sessions, deduped per device by server_msg_id.
  • Sequence gap on client: client blocks rendering past the gap and requests the missing seq, guaranteeing contiguous order.
  • Presence flapping: presence is TTL heartbeats; a missed heartbeat marks last-seen rather than hard-offline, avoiding flicker.
  • Group ordering: each member reads the shared conversation sequence, so all members see the same order; per-member delivery state (delivered/read) is tracked separately.
  • Message expiry: undelivered messages for a permanently-gone device expire after a bound (e.g. 30 days) to cap inbox growth.
  • E2E encryption: the server stores/forwards ciphertext (body BYTEA) and never sees plaintext; ordering, dedup, and receipts all work on metadata, not content.