Skip to lesson
‹

Courses / System Design Interview / Chapter 31

Module 3 · Design Real Products · Chapter 31 of 39

Design a Group Chat App

20 sections · ~42 min✓ Fact-checked

Builds on Back-of-the-Envelope Estimates, Load Balancers, Databases: Replication, Partitioning and Choosing a Store, Consensus, Leader Election and Coordination, Content Delivery Networks, Unique IDs and Sequencers, Message Queues, Publish-Subscribe and Blob Stores. Next up: Design a Web Crawler.

Part 1 · Requirements and Scale

The brief

11:59 pm on New Year’s Eve. In the Fest crew group, Asha asks “Who’s bringing the cake?” Ravi replies “Me!” and Meera replies “Not me.” On Kabir’s phone, “Me!” appears above the question. On Ravi’s laptop, Meera’s reply shows twice. And Asha’s next message, ticked as sent, never reaches anyone: the server holding it restarted under the midnight load. Three bugs, one cause. The app had no single agreed order for the chat, and no durable record of who was still owed each message.

You are building a chat app for phones and laptops. People message one to one and in groups of up to 500, share photos, and see when messages are delivered and read. A message must arrive instantly when both people are online and must never be lost when one of them is not. About 200 million people use it every day.

What we are building

Before sizing anything, pin down what the app does. Conversations are either one-to-one or groups of up to 500 members: a family group, a hostel wing, or the Fest crew from the opening. Here is the scope for this design:

In scopeOut of scope
One-to-one and group chats, up to 500 membersVoice and video calls
Text, plus photos and files as attachmentsPublic channels with millions of members
Sent, delivered and read receiptsSearch across all message history
Offline delivery with push notificationsBots and payments inside chats
History synced across a phone and a laptopStories and status updates
Online status and “typing...” indicatorsDisappearing messages

Two properties matter more than anything else in that list. Messages in a conversation must appear in the same order for everyone, and a message the server has acknowledged must never be lost, even if the recipient’s phone stays off for a week. The rest of the chapter is built around those two promises.

Group size decides the fan-out

A 10-person group and a 100,000-member channel need different designs. A small group can afford one copy per member; a huge channel must publish once to a topic. The 500-member cap is what lets this design deliver to every member individually.

The next question is how fast, how safe and how private the app must be, and how much traffic it carries.

How well it must work

Chat is judged on speed and trust. Turn both into rules you can check:

  • Low latency. With both people online, a message arrives in under 500 ms end to end, and usually far less within one country.
  • Durable and ordered. No acknowledged message is ever lost, and each conversation has one agreed order.
  • At-least-once delivery, shown once. When an acknowledgement is lost, the sender cannot tell a lost message from a lost ack, so it resends, and a message can arrive twice. Phones recognise what they already hold and show each message once.
  • Highly available. A chat server crash should cost its users a few seconds of reconnecting, never their messages.
  • Private. Only the people in a conversation can read it, using end-to-end encryption, covered at a high level later.

The estimates on the next page rest on these assumptions:

AssumptionValue
Daily active users200 M
Messages sent per user per day40
Users online at the same time at peakabout 30% of daily users
Connections one chat server holds100,000, a cautious planning figure
Average message size with metadataabout 200 bytes
Average recipients per message3 (1 in direct chats, more in groups)
Peak traffic over the daily averageabout 5×, for a festival night

Two of these drive the design: the share of users online at once sets the number of open connections, and recipients per message turn each send into several deliveries.

Estimates: messages, connections and storage

With the assumptions set, size the two things chat servers actually feel: deliveries per second and open connections.

Try it first: 200 M daily users each send 40 messages, and each message reaches 3 recipients on average. How many messages and deliveries per second is that, and what is the peak at 5×?
  • 200 M × 40 = 8 B messages a day; ÷ 86,400 s ≈ 93,000 messages a second.
  • × 3 recipients ≈ 280,000 deliveries a second on average.
  • × 5 at New Year’s midnight ≈ 465,000 sends and 1.4 M deliveries a second: size the delivery path for this, not for the average.

Plan for the peak, because at midnight on New Year’s Eve everybody greets everybody at once. That 5× is the low end of the estimates chapter’s festival range, so the design also needs a plan for shedding extras when traffic goes higher. The rest of the sizing follows from the same assumptions:

QuantityWorkingResult
Open connections at peak200 M users × 30% online≈ 60 M WebSockets
Chat servers60 M ÷ 100,000 connections each≈ 600, plus spares
Session directory60 M entries × about 100 bytes≈ 6 GB, small enough for memory
Message storage8 B messages × 200 bytes≈ 1.6 TB/day, ≈ 580 TB/year
With 3 replicas580 TB × 3≈ 1.75 PB/year
Attachments5% of messages × 200 KB≈ 80 TB/day, about 50× the text

Two numbers shape the design: 1.4 M deliveries a second and 60 M open sockets. Storage is large but ordinary, and the bytes are in attachments, which belong in a blob store.

How many connections fit on one server

Well-tuned servers can hold hundreds of thousands of mostly idle connections, because a quiet connection costs mostly memory, not CPU. 100,000 per server is a cautious assumption that leaves room for bursts and for taking over the users of a crashed neighbour.

Part 2 · Connections, API and Data

Connections and the session directory

Sixty million open connections raise the first question: how does a server push a message to a phone that did not ask for it? Plain HTTP cannot, because the server only answers when the phone asks. There are three common ways around that:

Short pollingLong pollingWebSocket
How it worksAsk “anything new?” every few secondsAsk, and the server holds the request open until something arrives or about 30 s passOne connection stays open; either side sends at any time
DelayUp to the polling intervalLowLowest
WasteMost requests return nothingA fresh request after every messageA small heartbeat every 30 s or so
How it works

Short pollingAsk “anything new?” every few seconds

Long pollingAsk, and the server holds the request open until something arrives or about 30 s pass

WebSocketOne connection stays open; either side sends at any time

Delay

Short pollingUp to the polling interval

Long pollingLow

WebSocketLowest

Waste

Short pollingMost requests return nothing

Long pollingA fresh request after every message

WebSocketA small heartbeat every 30 s or so

WebSockets win for chat because an idle connection costs a heartbeat, not a stream of empty replies; long polling is the fallback where networks block them.

A WebSocket starts as an HTTPS request that asks to upgrade the connection; after the server answers 101 Switching Protocols, the same TLS-encrypted TCP connection carries small frames both ways. The load balancer picks a chat server when the connection opens, usually the least loaded one (see the load balancer chapter), and the phone stays there until the connection drops. Both sides send a heartbeat, such as a ping every 30 seconds, because a phone that loses signal sends no goodbye: two missed heartbeats tell the server within about a minute that the connection is dead.

With 600 chat servers, Asha’s message usually reaches a different server from the one holding Ravi’s connection. A session directory answers “which chat server holds user X right now?”. It is an in-memory key-value store, sharded by user id, that maps each connected device to a chat server. A server writes the entry on connect and deletes it on disconnect. A crashed server cannot clean up, so entries expire after a couple of minutes unless refreshed, and a dead server’s entries simply run out.

Why the socket closes in the background

Phone operating systems suspend background apps to save battery, and their connections soon drop. The platform’s push service keeps one shared connection per phone for all apps, so offline delivery relies on it rather than on your own socket.

API and data model

Next, decide what travels over the connection and where it is kept. Most traffic is small WebSocket frames; a few HTTP calls handle the chat list, history, new groups and attachments:

http
GET  /v1/connect   Upgrade: websocket   -> 101 Switching Protocols, then frames both ways
Asha -> {"type": "send", "conv": "c_88", "client_msg_id": "a1f3", "body": "<ciphertext>"}
Asha <- {"type": "ack", "client_msg_id": "a1f3", "seq": 39}
Ravi <- {"type": "message", "conv": "c_88", "seq": 39, "from": "u_asha", "body": "<ciphertext>"}
Ravi -> {"type": "receipt", "conv": "c_88", "upto_seq": 39, "kind": "delivered"}
GET  /v1/inbox?limit=50   -> 200 [{"conv": "c_88", "last_seq": 40, "unread": 2}, ...]   (newest first)
GET  /v1/conversations/c_88/messages?after_seq=39&limit=100   -> 200 {"messages": [...]}
POST /v1/conversations  {"members": ["u_ravi", "u_meera"], "title": "Fest crew"}  -> 201 {"conv": "c_91"}
POST /v1/attachments    -> 200 {"put_url": "...", "key": "att_7q2"}   (upload straight to the blob store)

Asha -> is Asha’s phone sending; Ravi <- is the server sending to Ravi’s phone. Bodies are encrypted on the phone; the socket is authenticated when it opens.

Two ids do the work. The client_msg_id is made by the phone, so a resend after a timeout is recognised as the same message and answered with the original seq. The seq is assigned by the server from one counter per conversation, and it fixes the order everyone sees. Four tables hold the state:

conversationsconv_id (pk)kind (direct, group)titlelast_seqcreated_at
membersconv_id (pk part 1)user_id (pk part 2)rolelast_delivered_seqlast_read_seqjoined_at
messagesconv_id + bucket (partition key)seq (sort key)client_msg_idsender_idbody (ciphertext; empty in a tombstone)attachment_keysent_at
user_inboxuser_id (partition key)last_activity (sort key, newest first)conv_idlast_seq

Messages are written once and read in order within a conversation, so a wide-column store from the databases chapter fits: partition by conversation, sort by seq, keep three replicas. The key also carries a bucket number, seq ÷ 100,000, so no partition grows past about 20 MB of text. Attachments go to a blob store through signed upload URLs and are served by a CDN.

user_inbox is each user’s chat list, sorted by last activity, so a phone loads its 50 most recent chats with one range read. The unread badge is last_seq − last_read_seq, two numbers the design already keeps. A stored counter would cost a write per member per message and drift whenever a retry delivered twice.

High-level design

With the data in place, put the pieces together and follow one message from Asha to Ravi. Phones hold WebSockets to chat servers behind a load balancer. For each conversation, one chat server acts as its owner: it numbers each message, writes it to the message store and to a fan-out log, and only then acknowledges it. Fan-out workers deliver from the log, and a push queue wakes phones whose app is closed. The parts, in one list:

Follow one message, first while Ravi is online and then while his app is closed.

Animated sceneOnline, offline, then sync0:29

Watch for the order of steps: stored first, then “sent”, then delivered or pushed

Read it as steps

Watch for the order of steps: stored, then acknowledged, then delivered live, or announced by a push that Ravi’s phone follows with a sync.

Three rules carry the design. Store before you acknowledge, so once Asha sees “sent”, no crash can lose the message. Deliver to a socket when you can, send a push when you cannot, and on reconnect let the phone sync from its cursor, the last seq it holds in each chat.

The picture hides two hard questions: who hands out the seq, and what happens when that server or a fan-out worker dies mid-message. The deep dives take them in turn.

Part 3 · Deep Dives

Ordering messages

Core deep dive

Ordering is the hardest part of chat: two people sending at once, and a server dying mid-send, break designs that look fine on paper.

Start with the simpler half. If Ravi and Meera both reply in the same second, how does every member see the two replies in the same order? Phone clocks cannot decide it: they drift by seconds, and some are simply set wrong. Here are three Fest crew messages whose clocks disagree:

MessageSender’s phone clockArrives at serverseq given
Asha: “Leaving now”18:00:0918:00:02.341
Ravi: “Wait for me”18:00:0118:00:05.242
Meera: “Bus is here”18:00:0518:00:05.643
Asha: “Leaving now”

Sender’s phone clock18:00:09

Arrives at server18:00:02.3

seq given41

Ravi: “Wait for me”

Sender’s phone clock18:00:01

Arrives at server18:00:05.2

seq given42

Meera: “Bus is here”

Sender’s phone clock18:00:05

Arrives at server18:00:05.6

seq given43

Sorted by phone clocks, Ravi’s reply would appear above the message it answers, and Asha’s would come last. Instead, the server gives each conversation its own counter: every accepted message takes the next seq, and every device sorts by it. A reply is sent only after its sender has seen the original, so it always gets a higher seq. Ravi’s and Meera’s messages could land in either order; what matters is that everyone sees the same one.

Sequence numbers also reveal gaps. A phone that holds 41 and receives 44 knows it is missing 42 and 43, so it fetches them before showing 44. Time-ordered ids like those in the unique IDs chapter cannot do this: they are unique and roughly sorted, but nothing tells you an id in between is missing.

Order within a conversation only

Messages in different chats may arrive in any order relative to each other. A global order would need one counter for the whole system: a bottleneck and a single point of failure that gives users nothing.

A counter is easy on one machine. The next page asks what happens when the machine holding it fails.

One owner per chat: the lease and the fence

A counter needs exactly one owner at a time. Conversations are split into shards by consistent hashing, and each shard’s owner, a chat server, holds a lease on it: a right from a coordination service that expires unless renewed, such as 10 seconds renewed every 3 (see the coordination chapter). The owner keeps each chat’s next seq in memory, so numbering costs no database round trip; even at the 465,000-a-second peak, each owner numbers under 1,000 sends a second.

When an owner dies, its lease runs out and another server takes the shard. The dangerous case is an owner that only freezes, in a long pause, and wakes still sure it owns the shard.

Predict before you watch: the old owner wakes and tries to store Meera’s message as seq 44, a number the new owner has already used. What happens to her message?
  • The store refuses it, because seq 44 already holds Ravi’s message.
  • The old owner steps down; Meera’s phone, still without an ack, resends to the new owner and gets 45.
Animated sceneOne owner per chat, fenced by the store0:27

Ravi and Meera reply at once. The owner numbers them 42 and 43 and stores each if free

Read it as steps

Watch for the old owner’s late write of seq 44: the store refuses it because 44 is taken, so no number ever names two messages.

The fence is in the store, not the lease. Every message is written with insert-if-absent on (conv_id, seq), a conditional write that wide-column stores offer for a few extra milliseconds, so no seq can ever hold two messages. Like the coordination chapter’s fencing token, it lets the store decide who wins. If the old owner’s late write reaches a still-free seq first, it simply wins and the new owner re-reads; the old owner stops at its next failed renewal, within about 3 seconds.

Timeouts, gaps and resends

The fence keeps every seq unique. Two smaller cracks remain, and each could hide or duplicate a message: a write whose outcome is unknown, and a resend that reaches a new owner.

A write that times out. As the abstractions chapter showed, a timeout does not say whether the write happened. If the owner simply skipped that seq and phones stopped waiting for it, a write that landed later would sit where no phone looks, and Asha would have been told “sent”. So the owner settles every failed seq: it writes a tombstone, an empty placeholder, at the same seq with the same insert-if-absent, retried until it gets an answer. Either the tombstone lands, and nothing can be written there ever again, or the original write had landed after all, and the owner acknowledges the message with that seq.

Phones never skip a missing seq. A phone that holds 50 and receives 52 fetches 51 and waits until it is either a message or a tombstone: usually milliseconds, at worst one takeover. It skips only tombstones, so an acknowledged message can never hide behind a phone’s cursor.

A resend that reaches a new owner. Say the old owner stored Asha’s message as seq 60, then died before the ack reached her. Her phone resends with the same client_msg_id. A new owner that knew only the highest seq would store the message again as 61, and every member would see it twice. So before numbering a chat’s first message after a takeover, the new owner reads that chat’s recent tail, the last few hundred messages with their client_msg_ids. It answers a resend of a stored message with the original seq, and tombstones any gap it finds in the tail, settling writes the old owner left open.

Reading the tail costs one range read per chat, a few milliseconds, paid once per takeover. Put together, a takeover pauses sends in that shard’s chats for about one lease, roughly 10 seconds, and never loses or doubles a message. Ordering is settled; the next question is how one stored message reaches all 500 members.

Fan-out that never loses a message

A stored message must reach every member even if a server crashes halfway, so no chat server fans out from memory. After the store write, the owner appends the message to a durable fan-out log, partitioned by conversation as in the message queue chapter, and only then acknowledges the sender. If the owner dies between the two, the sender resends, and the new owner finds the stored message by its client_msg_id and logs it then.

Animated sceneFan-out from a durable log0:21

Asha’s message is stored as seq 46, appended to the log, and only then acknowledged

Read it as steps

Watch for the repeat after the worker dies: it resumes from its last committed offset, and phones drop seq 46 because they already have it.

The log turns a crash into a repeat, never a loss. Workers commit their offset, the log’s bookmark, only after forwarding, so a replacement redoes unfinished work and phones drop any seq they already hold. Delivery is at least once; display is exactly once.

For each message a fan-out worker looks up the members; moves the chat to the top of each member’s chat list; asks the session directory which servers hold their devices; forwards one copy per server; and queues a push for anyone offline. One worker per partition keeps each chat in log order. How much work one message causes depends on group size:

Group sizeFan-out approachWhy
2 (a direct chat)Look up one user, forward onceThe cheapest case
Up to a few hundredLook up every member; forward one copy per chat serverBounded work: at most a few hundred deliveries
Thousands or more (channels)Publish once to a topic; servers with subscribed members deliver; others fetch on openAvoids huge fan-out per message, as in the pub-sub chapter

A 500-member group spread over hundreds of servers costs about 500 deliveries and 500 chat-list updates per message, which is why groups have a size cap.

Offline delivery and reconnect sync

Fan-out finds no live session for some members. What do their phones get, and how do they catch up without missing or repeating anything?

For each such member, the worker puts a small job on a push queue: recipient, conversation and seq. Push workers call the phone platform’s push service. The push carries no readable text: with end-to-end encryption it holds either the still-encrypted message for the phone to decrypt or just a signal that something is waiting. One push per chat is enough until the chat is opened, and muted chats get none.

Push is best effort: it can arrive late, out of order or not at all, so it is only a nudge. The message store, not the push, is the source of truth. When the app opens, it runs reconnect sync:

Reconnect sync
  1. 1Registerthe new chat server writes the device into the session directory, so live messages flow again at once
  2. 2Read the chat listuser_inbox rows changed since the last sync show which chats moved
  3. 3Compare cursorsfor each of those chats, the phone’s cursor, the last seq it holds, against the chat’s last_seq
  4. 4Fetch only the gapmessages after the cursor, 100 at a time; live messages arriving meanwhile merge by seq
  5. 5Report receiptsone “delivered up to” per chat, not one per message

The cost is small. Ravi’s phone, off for a weekend, might hold 2,000 new messages across 30 chats: about 400 KB of text in a few dozen range reads, well under a second on a mobile network.

Each device keeps its own cursors, so a laptop that was off for a week catches up from exactly where it stopped. Read state belongs to the user: reading on the laptop sends “read up to seq 87”, which clears the badge on the phone. The same sync makes a server crash cheap: its 100,000 phones reconnect elsewhere and each fetches only its own gap.

Receipts and presence

Synced phones report back: delivered, then read. How do you record those reports for 500 members without multiplying the work? Each receipt has a trigger and a storage form:

ReceiptSent whenStored as
SentThe server has stored the message and assigned a seqNothing extra: it is the ack to the sender
DeliveredA recipient’s device has received itThe highest delivered seq per member
ReadThe recipient opened the chat past that messagelast_read_seq per member

Store receipts as watermarks, “everything up to seq N”, not one row per message per member. In a 500-member group, per-message rows would add about 1,000 rows, delivered and read, for every message; watermarks stay at two numbers per member however long the chat runs. Forwarding every receipt to the sender would double the fan-out, so in big groups the server updates the watermarks, tells the sender once everyone has the message, and builds the “read by” list only when someone opens it.

Presence, the online dot and “last seen”, is chattier than it looks. If 60 M online users each changed state 10 times an evening and every change went to 200 contacts, that would be about 8 M updates a second over four hours, more than five times the peak message deliveries. So presence is read, not pushed: the directory knows who is connected, the server saves “last seen” when a user’s last device disconnects, and a phone asks only about the people in the chat it has open.

Typing indicators go only to members with the chat open, are never stored, and are the first thing dropped under load. Next, put the whole design under stress: what fails, and what users notice.

Part 4 · Failures and Trade-offs

What fails, and how the design copes

With the design complete, walk through each moving part and what a user would notice if it failed:

What goes wrongWhat users noticeHow the design copes
A chat server crashesA few seconds of reconnectingPhones reconnect after a short random delay, so 100,000 do not arrive at once, then register and sync from their cursors
A deploy restarts the fleetNothing, if it is pacedDrain a few percent of servers at a time; their phones reconnect elsewhere with random delays spread over a minute or two, never 60 M at once
A shard owner freezes or diesSends in its chats pause for about 10 sThe lease expires; the new owner reads each chat’s tail; insert-if-absent rejects late writes
An owner’s store write times outA short wait for that messageA tombstone written if absent settles the seq either way; phones never skip it unsettled
A fan-out worker dies mid-messageNothing; repeats are hiddenA replacement resumes from the last committed offset; phones drop seqs they hold
A directory entry points at a dead serverA push instead of a live message, brieflyThe forward fails fast, the worker queues a push, and the stale entry expires
The push service drops a pushA late notification, or noneThe message is safe in the store; the app syncs when opened
A zone or region failsA zone: a quick reconnect. A region: its chats pause a minute or twoWrites reach two of three zones before the ack. A standby region holds every message; phones reconnect there and its servers take the shards
The New Year surgeSlower deliveryCapacity for 5×; beyond that, typing, presence and receipts are shed so messages keep flowing

Every row ends in a reconnect, a pause or a hidden repeat, never a lost acknowledged message, because the store and the log hold everything and chat servers hold nothing they cannot rebuild.

Trade-offs, cost and what to watch

Each guarantee in that table has a price. Most trade-offs come down to what to drop first under pressure: messages never; read receipts can be delayed; typing indicators and online status can be skipped; history sync for old chats can be paced.

The second trade-off is where state lives. Chat servers hold only connections and, for the shards they own, each chat’s next seq, and both are cheap to rebuild. The price is a conditional store write and a log append on every send, a few milliseconds that buy crash safety.

Going global adds a third. Give each conversation a home region, usually where most members are, that owns its shard. Members elsewhere connect to nearby chat servers, which forward to the home region, so a message between Delhi and London pays one long round trip of about 100 to 150 ms, still inside the 500 ms budget. Copy each message to the standby region before the ack: about 30 ms more between two Indian regions buys zero loss when a region dies.

Retention. Text is small, about 580 TB a year before replicas, so keep it as long as the chat exists. Attachments are 50 times bigger and rarely opened after a few weeks, so move them to a colder, cheaper tier after about 30 days. Three signals show trouble early:

What dominates the bill: attachments. Storing about 80 TB of new photos and files a day and serving each to every recipient usually costs more than the text and the chat servers together, which is why old media moves to a colder tier.

Part 5 · Going further

End-to-end encryption at a high level

Optional: what changes when even the servers must not read the messages?

With end-to-end encryption, messages are encrypted on the sender’s phone and decrypted only on the recipients’ devices. Private keys never leave those devices, so servers store and forward ciphertext they cannot read. The design barely changes, which is why the API showed <ciphertext> bodies from the start. A direct chat works like this, in outline:

Encrypting a direct chat, in outline
  1. 1Each device makes its own key pairthe private key stays on the device; the public key goes to a key directory
  2. 2The sender fetches the recipient’s public keysone per device, so the phone and the laptop each get their own copy
  3. 3Both sides compute the same shared secretfrom their own private key and the other’s public key; the secret never crosses the network, and the recipient can be offline
  4. 4Each message gets a fresh keyderived from that secret and changed as the chat goes on, so one stolen key exposes little
  5. 5The server stores and forwards ciphertextit sees who talks to whom, when and how much, but not what is said

Groups are harder: a separate copy for every member’s device would mean hundreds of copies per message. A common approach gives each sender their own key for the group, often called a sender key, shared once with every member’s devices over their one-to-one encrypted channels. Each message is then encrypted once, and the server fans out the same ciphertext. When someone leaves, the others make new keys and share them with everyone but that person; a new member gets only the current keys, so they cannot read older messages.

A new device, such as a laptop, makes its own key pair and is approved from a device the user already has. Attachments are encrypted on the phone with a fresh key that travels inside the message, so the blob store and CDN hold only unreadable bytes. The protection has costs elsewhere:

  • The server cannot search content, so search runs on the device.
  • Spam detection relies on metadata, such as sending rates, and on user reports.
  • Backups must be encrypted with a key only the user holds, or they undo the protection.

Part 6 · Interview kit

The 45-minute answer

A 45-minute round leaves about 40 minutes of design. Here is this chapter on that clock:

MinutesWhat you say and drawKey number or decision
0–5Requirements: one-to-one and groups up to 500, receipts, offline delivery, phone and laptop. Out of scope: calls, huge channels.Same order for everyone; never lose a sent message; under 500 ms
5–8Estimates that decide something: deliveries per second and open connections. Storage gets one line.1.4 M deliveries/s at peak; 60 M sockets ≈ 600 servers
8–13API: WebSocket frames for send, ack, message and receipt; HTTP for the chat list and history. Data: messages by conversation and seq, members with watermarks, user_inbox.client_msg_id makes resends safe; seq fixes the order
13–22Draw phones, load balancer, chat servers, session directory, message store, fan-out log and workers, push queue, blob store and CDN. Walk one message online, then offline.Store, log, then acknowledge
22–37Deep dive: a leased owner per chat, insert-if-absent, tombstones for failed writes; then fan-out from the log with offsets; receipts as watermarks if time is left.One owner per chat; settle every seq; commit after delivery
37–40Wrap-up: a server crash, a deploy, a region loss, what is shed first, and what you would add next.Shed typing and presence long before messages

The owner, the fence and the log in the middle rows are what make this design hold up. Pick ordering as the deep dive if they ask about two people sending at once or a server dying mid-send; pick fan-out if they ask how one message reaches 500 members, or what offline members get.

Say
“At 1.4 M deliveries a second at peak, I store each message and log it before the ack, so a crash can delay a message but never lose it.”
Say
“Each chat has one leased owner that numbers messages in memory, under 1,000 sends a second per owner at peak; insert-if-absent fences a stale one.”
Say
“60 M open sockets at 100,000 per server is about 600 chat servers, and a 6 GB session directory maps users to them.”
Avoid
“Messages are ordered by timestamp.” Phone clocks disagree by seconds, so a reply can appear above the message it answers.
Avoid
“The chat server forwards to everyone, and we are done.” A crash halfway loses the message for some members; fan out from a durable log.

Interview drill

Interviewers change one thing and watch you adapt. Answer each before you open it.

It is New Year’s midnight and traffic is 10× the average, twice what you planned for. What bends first, and what do you do?
  • Sends reach about 930,000 a second and deliveries 2.8 M; the fan-out log absorbs the burst, so messages arrive late rather than get lost.
  • Shed typing and presence first, then delay read receipts; never drop messages, and collapse pushes to one per chat.
  • New Year is on the calendar: add chat servers, store nodes and fan-out workers the day before, up to one worker per log partition.
Product adds reactions and lets people edit a message for 15 minutes. What changes in the data model?
  • Edits and reactions become events in the same conversation, each with its own seq and the seq of the message it changes, so cursor sync and de-duplication work unchanged.
  • Unread must count messages, not events: give messages a second counter that edits and reactions skip.
  • The owner checks the sender and the 15-minute window before accepting an edit; reactions travel like receipts.
Finance asks you to halve the cost. Where do you look first?
  • Attachments dominate: compress photos on the phone, reuse the stored file when a photo is forwarded, and move media older than 30 days to an archive tier 20 to 50 times cheaper.
  • Tune chat servers to hold 200,000 idle sockets instead of 100,000, roughly halving the fleet, while keeping room for a dead neighbour’s users.
  • Never cut the copies of acknowledged messages: losing one breaks the promise users notice most.
Common miss
Treating the push as delivery. Pushes are best effort; the app must sync from its cursor when it opens.
Common miss
Skipping a missing seq after a timeout. A late write would then hide behind the cursor; settle it with a tombstone.

Wrap-up

Next, Design a Web Crawler turns the problem around: nobody waits at a screen, but millions of websites must be visited politely and billions of pages kept fresh.

Part 7 · Check yourself

Quick check

Q1A design has the sender’s chat server forward each group message straight from memory. After a crash, some Fest crew members never get a message Asha saw as “sent”. What is the flaw, and the fix?

Think it through, then check.

Q2Product wants channels with 200,000 members, where a few admins post and everyone reads. What changes?

Think it through, then check.

Q3The standby region can receive each message before the ack or just after it. Which do you choose for chat, and what does each cost?

Think it through, then check.

Q4Ravi’s phone holds seq 50 of the Fest crew chat and has just received 52, but 51 has not come. What might have happened, and what must the phone not do?

Think it through, then check.

Q5Which building block sits between storing a message and delivering it to 500 members, and why that one?

Think it through, then check.

Next chapter · 32 of 39Design a Web Crawler~41 min · 2 animated scenes