Skip to main content

Design WhatsApp

A messaging service is a direct, concrete application of persistent connections and delivery guarantees — the interesting problems aren't the UI, they're making sure a message actually reaches its recipient, in order, exactly once, even when that recipient is offline when it's sent. Following the framework from How to Answer a System Design Interview Question.

1. Requirements​

Functional:

  • Send and receive one-to-one and group messages.
  • Show delivery status (sent, delivered, read) and online/last-seen presence.
  • Deliver messages sent while a recipient was offline as soon as they reconnect.

Non-functional:

  • Messages must not be lost, and should arrive in order per conversation.
  • Low latency for online users — a message should arrive close to instantly if both parties are connected.
  • Must scale to millions of concurrent persistent connections, not just requests per second.

Scale estimate: assume 500M daily active users, each holding one persistent connection while the app is open, and an average of 40 messages/user/day (~20B messages/day, ~230,000 messages/sec at peak). The dominant infrastructure cost here isn't request throughput — it's the sheer number of simultaneously open connections.

2. API Design​

Primarily connection-based rather than request/response — a client opens one persistent connection and both sends and receives over it:

WS /connect -- persistent connection, authenticated once
send: { to: user_id, body: string, client_msg_id: string }
receive: { from: user_id, body: string, msg_id: string, sent_at: timestamp }
receive: { type: "ack", msg_id, status: delivered|read }

3. Data Model​

messages
msg_id VARCHAR PRIMARY KEY
conversation_id VARCHAR
sender_id VARCHAR
body TEXT
sent_at TIMESTAMP
status ENUM(sent, delivered, read)

conversations
conversation_id VARCHAR PRIMARY KEY
participant_ids VARCHAR[]

conversation_id as a partition key (rather than sharding by individual message) keeps every message belonging to one conversation together, matching the dominant access pattern: "load the last N messages for this conversation," which would otherwise need to fan out across shards if messages were partitioned independently.

4. High-Level Design​

System Design Lab

Each client holds a WebSocket to a connection gateway — the concrete instance of that lesson's core tradeoff (statefulness, for the capability plain HTTP can't offer: the server pushing a message the instant it arrives). A message queue sits between the gateway and durable storage specifically so a message is never lost even if the recipient's gateway connection or the storage write briefly hiccups — the queue buffers exactly the gap Message Queues describes between a fast producer and a momentarily slower consumer.

5. Deep Dive: delivering to an offline recipient, exactly once​

Because each client holds a stateful WebSocket connection, a message for an offline recipient has nowhere to be pushed to — this is precisely the WebSockets lesson's core statefulness cost: a client is pinned to whichever gateway server holds its connection, and that connection simply doesn't exist while offline. The fix has two parts: the message is durably persisted to the message store regardless of the recipient's connection state, and a push notification — the mobile OS's own push channel, not the app's WebSocket — alerts the recipient's device; when the recipient reconnects, the client fetches any messages after its last-seen msg_id from the store.

This redelivery path is exactly the at-least-once problem Message Queues names generally: a message can be resent if an acknowledgment was lost even though it actually arrived, which is why the client-generated client_msg_id in the API matters — it's the idempotency key that lets the client (and the server) safely de-duplicate a message that arrives twice, rather than showing it in the conversation twice.

6. Deep Dive: fanning a message out to every one of a recipient's devices, in order​

Real accounts aren't pinned to one device — a message needs to reach a recipient's phone, linked desktop app, and web client all at once, and every device's view of a conversation has to agree on the same order even though each connects and disconnects independently. That's two problems layered on top of the single-device design above:

Fan-out. The connection gateway so far assumes "one recipient, one connection." Supporting multiple devices means a recipient's user_id maps to a set of active gateway connections — one per linked device — and a new message has to be pushed to all of them, not just the first one found. This is the same one-to-many delivery shape as Pub/Sub: each device subscribes as a consumer of that user's message stream, and one device's connection dropping shouldn't block, or slow down, delivery to the others.

Ordering. If two devices each briefly buffer messages while reconnecting at different times, they still have to converge on the same order once caught up — not whatever order each happened to receive pushes in. The fix reuses the same idea as client_msg_id above, but for ordering instead of deduplication: every message gets a conversation-scoped, monotonically increasing sequence number, assigned once by the server at write time, never by a client. "Sort by sequence number" then gives every device an identical, deterministic view of the conversation, regardless of the order individual push notifications actually happened to arrive in.

System Design Lab

The reconnect step is deliberately the same mechanism as the single-device offline case above, just applied per device instead of per user — one design serving both problems is a good sign it's the right one, not a coincidence.

7. Tradeoffs​

Persisting every message before considering it "sent" (rather than only relaying it live and hoping the recipient is connected) is strictly more work and adds a small amount of latency, but it's the only way to honestly satisfy the "messages must not be lost" requirement — a purely relay-based design would silently drop anything sent while the recipient was offline, which fails the actual requirement outright rather than just being a slower way to meet it.

That cost also isn't fixed per message — it scales with fan-out. A one-to-one message writes once and pushes to at most a handful of devices, but a message to a 500-person group multiplies both: one write per recipient's inbox view (or a read-time fan-in, the same fan-out-on-write vs. fan-out-on-read choice a social feed faces) and up to 500 × (devices per person) pushes for one send. Large groups are exactly where a naive "loop over every member and push" implementation stops being merely slow and starts being the system's actual bottleneck, which is why real messaging systems treat very large groups as a distinct, separately-optimized path rather than the same code as a 1:1 chat.

Persist-then-deliver vs. relay-only messaging: pros and cons​

Pros

  • Messages sent while the recipient is offline are never lost
  • A reconnecting client can always catch up from its last-seen point
  • Delivery and read receipts have a durable record to be computed against

Cons

  • Adds a write to durable storage on the critical path of every message
  • Requires a queue or buffer between the connection layer and storage
  • More infrastructure than a simple relay that only works while both parties are online

Further Reading​

Share this lesson

Saved locally in your browser — visible in the sidebar as you go.