Skip to the solution
HardSystem Design15 min read

How would you design a real-time chat system?

876 views

The problem

Question presented to candidate: "Design a chat app like WhatsApp or Messenger: one-to-one and group conversations, messages delivered in real time when the other person is online, nothing lost when they are offline, and history synced across a user's devices."

What a strong answer should cover:

  • Requirements and capacity: messages per second, storage per day, concurrent connections and how many gateway servers they need
  • Persistent connections (WebSocket) to a gateway tier, a session registry mapping users to gateways, and routing a message to the recipient gateway; the message is stored before it is acknowledged
  • Per-conversation sequence numbers for ordering, client message ids so a retried send is stored once, and sync from the last sequence seen for offline users and new devices
  • Gap detection on the client (held messages, gap timer, fetch the missing range) and push notifications for offline users
  • Storage keyed by conversation and sequence in a wide-column store, group fan-out per gateway rather than per member, and what breaks at 10x (very large groups, hot conversations, reconnect storms)

Clarifying questions expected:

  • One-to-one only, or groups too? How large can a group be?
  • Must history be stored on the server and synced to many devices, or only relayed (store-and-forward until delivered)?
  • Which receipts are needed: sent, delivered, read? Typing indicators and presence?

Code / implementation expected: Optional โ€” a simulation of the delivery protocol: sequence numbers, a lost ack and a deduplicated retry, offline sync, a delayed and a dropped message repaired by the client; the example does that.

  • chat

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

15 min read ยท 16 sections

Read the code

Chat delivery: per-conversation sequence numbers, idempotent retries, offline sync, gap repair and per-gateway group fan-out
// Chat delivery: per-conversation sequence numbers, idempotent sends, offline sync, gap repair, group fan-out.
const fmt = (n) => n.toLocaleString("en-US");
console.log("== 1. Capacity (assumptions: 500M DAU, 40 messages/user/day, 300 bytes stored per message, RF 3) ==");
const msgsDay = 500_000_000 * 40;
console.log("messages/day " + fmt(msgsDay) + ", avg " + fmt(Math.floor(msgsDay / 86400)) + "/s, peak (x3) " + fmt(Math.floor(msgsDay / 86400) * 3) + "/s");
console.log("storage/day " + fmt(msgsDay * 300 / 1e12) + " TB raw, " + fmt(msgsDay * 300 * 3 / 1e12) + " TB with 3 replicas");
const online = 500_000_000 * 30 / 100, perGw = 500_000;
console.log("peak concurrent sockets " + fmt(online) + " (30% of DAU) -> " + fmt(online / perGw) + " gateways at " + fmt(perGw) + " sockets each");

console.log("\n== 2. Delivery simulation (store = append-only log per conversation) ==");
const store = {}, seqOf = {}, dedup = {}, registry = {}, pushes = [];
let gatewaySends = 0;
const clients = {};
const client = (u) => (clients[u] ||= { lastSeq: {}, held: {}, shown: [] });
function apply(u, conv, m) { // client side: apply in order, hold anything after a gap
  const c = client(u), want = (c.lastSeq[conv] || 0) + 1;
  if (m.seq < want) return u + " ignores duplicate seq " + m.seq;
  if (m.seq > want) { c.held[m.seq] = m; return u + " holds seq " + m.seq + " (expected " + want + ", gap)"; }
  c.shown.push(m.seq + ":" + m.text); c.lastSeq[conv] = m.seq;
  let note = u + " shows seq " + m.seq;
  while (c.held[c.lastSeq[conv] + 1]) { const h = c.held[c.lastSeq[conv] + 1]; delete c.held[h.seq]; c.shown.push(h.seq + ":" + h.text); c.lastSeq[conv] = h.seq; note += ", then held seq " + h.seq; }
  return note;
}
function send(conv, members, from, clientId, text, opts = {}) {
  const key = conv + "/" + clientId;
  if (dedup[key]) return console.log("  " + from + " retry " + clientId + " -> already stored as seq " + dedup[key] + ", nothing re-sent");
  const seq = (seqOf[conv] = (seqOf[conv] || 0) + 1);
  const m = { seq, text };
  (store[conv] ||= []).push(m); dedup[key] = seq;
  const byGw = {};
  for (const u of members) if (u !== from) {
    if (!registry[u]) { pushes.push(u + ":" + conv); continue; }
    (byGw[registry[u]] ||= []).push(u);
  }
  const notes = [];
  for (const gw of Object.keys(byGw).sort()) {
    gatewaySends++; // one publish per gateway, which then writes to each local socket
    for (const u of byGw[gw]) {
      if (opts.drop === u) notes.push(u + " (dropped in transit)");
      else if (opts.delay === u) notes.push(u + " (delayed)");
      else notes.push(apply(u, conv, m));
    }
  }
  console.log("  " + from + " " + clientId + " '" + text + "' -> seq " + seq + (opts.ackLost ? " (ack lost)" : " acked") + (notes.length ? " | " + notes.join("; ") : " | recipient offline, push queued"));
  return m;
}
const dm = ["alice", "bob"];
registry.alice = "gw1"; registry.bob = "gw2";
send("dm1", dm, "alice", "a-1", "hi");
send("dm1", dm, "alice", "a-2", "lunch?", { ackLost: true });
send("dm1", dm, "alice", "a-2", "lunch?");
console.log("  stored messages in dm1: " + store.dm1.length);
delete registry.bob; console.log("  bob goes offline");
send("dm1", dm, "alice", "a-3", "12:30?");
send("dm1", dm, "alice", "a-4", "or 13:00");
registry.bob = "gw1";
const missed = store.dm1.filter((m) => m.seq > client("bob").lastSeq.dm1);
console.log("  bob reconnects to gw1, sync(after=" + client("bob").lastSeq.dm1 + ") returns seq " + missed.map((m) => m.seq).join(","));
for (const m of missed) console.log("  " + apply("bob", "dm1", m));
const late = send("dm1", dm, "alice", "a-5", "booked", { delay: "bob" });
send("dm1", dm, "alice", "a-6", "see you");
console.log("  " + apply("bob", "dm1", late) + "   (delayed copy arrives)");
send("dm1", dm, "alice", "a-7", "bring id", { drop: "bob" });
send("dm1", dm, "alice", "a-8", "thanks");
const gapFrom = client("bob").lastSeq.dm1;
const repair = store.dm1.filter((m) => m.seq > gapFrom && !client("bob").held[m.seq]);
console.log("  bob gap timer fires: fetch after seq " + gapFrom + " -> seq " + repair.map((m) => m.seq).join(","));
for (const m of repair) console.log("  " + apply("bob", "dm1", m));
console.log("  bob screen: " + client("bob").shown.join(" | "));
console.log("  push notifications queued: " + pushes.length + " (" + pushes.join(", ") + ")");

console.log("\n== 3. Group of 6: fan-out is per gateway, not per member ==");
const group = ["alice", "bob", "carl", "dana", "eve", "fay"];
registry.carl = "gw2"; registry.dana = "gw2"; registry.eve = "gw3"; registry.fay = "gw3";
gatewaySends = 0;
send("g1", group, "alice", "a-1", "standup in 5");
console.log("  recipients 5, gateway publishes " + gatewaySends + ", stored copies " + store.g1.length);

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 65 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

How would you design a file storage service like Dropbox?

Continue