The system design question
Design a chat app like Messenger. Support one-to-one and group chats, people who are offline, delivered and read receipts, and message history. Then tell me how it scales.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Sending messages in one-to-one and group chats | Voice and video calls |
| Delivering to people who are offline | Photos and files, beyond a mention |
| Delivered and read receipts | End-to-end encryption |
| Message history | Search across all messages |
Then agree on what "good" means:
- Never lose a message: once the sender sees it went through, it's saved.
- In order: everyone in a chat sees its messages in the same order.
- Quick: if the other person's online, it shows up in well under a second.
- Scales: hundreds of millions of people, most of them connected at once.
Rough numbers (5 minutes)
These are assumptions: 500 million daily users, 40 messages each a day, about 100 bytes of text per message, and groups capped at 500 people.
- ~230K
- messages per second on average
- ~2 TB
- of new messages a day
- ~100M
- open connections at a peak
- ~1M
- receipts per second
| Question | Rough math | What it means |
|---|---|---|
| How many messages? | 500 million × 40 ÷ 86,400 | About 230K a second, and maybe double at a peak |
| How much storage? | 20 billion a day × about 100 bytes | About 2 TB a day, so 700 TB a year. History has to be split across lots of machines |
| How many connections? | Say 100 million online at once ÷ 100K per server | About 1,000 servers just to hold connections |
| How many receipts? | Each message gets a delivered and a read, more in groups | A few times the message rate. They need to be cheap |
The overall design (10 minutes)
- Each phone keeps a WebSocket open to a chat server while the app's open. A small directory says which server each online user is on.
- When someone sends a message, their chat server saves it to the message store under that chat, with the next sequence number, and only then tells the sender it went through.
- For each other member, it looks them up in the directory. If they're online, it forwards the message to their chat server, which pushes it down their connection.
- If they're offline, the message waits in the store and we send a phone push. When they come back, the app asks for everything after the last sequence number it has, per chat.
- Delivered and read receipts go the other way as small events, and we save the latest one per person per chat.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Send a message | App, over the connection | Chat ID, a client message ID, and the text |
| Get new messages | App, when it reconnects | For each chat, the last sequence number it has |
| Load older history | App, scrolling up | Chat ID and a sequence number to read backwards from |
| Mark delivered or read | App | Chat ID and the sequence number it's reached |
| Create or change a group | App | Members to add or remove |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Messages | A wide-column store, keyed by chat then sequence number | Huge, write-heavy, read by chat in order |
| Chats and members | A relational database | Small, has relations, changes rarely |
| Each user's chat list, newest first | A key-value store keyed by user | The app's home screen |
| Read and delivered marks | Key-value, one row per person per chat | Overwritten in place, so it doesn't grow with messages |
| Who's connected where | An in-memory store with expiry | Routing live messages to the right server |
Step 1: Sending, ordering and offline delivery (12 minutes)
Save first, then deliver. The sender's server writes the message to the store before it tells the sender it worked. Once that's done, a crash can't lose it, because anyone who missed the live push gets it when they catch up.
Order comes from a sequence number per chat. The chat's partition hands out the next number when it saves the message, so every member sees the same order. Don't use phone clocks, since they disagree.
Phones retry when they don't hear back, so a message can arrive twice. The app makes up a message ID before sending, and the server ignores one it's already saved. That's idempotency, and it means retries are always safe.
Coming back online
The app remembers the last sequence number it has for each chat. When it reconnects, it sends those, and the server returns anything newer. It's simple, and it works the same after five seconds offline or five days. While they're away, a phone push tells them there's something new.
Step 2: Groups and receipts (8 minutes)
A group message is saved once, under the group's chat ID. Only the delivery fans out: one forward per online member, one push per offline one. With a 500-person cap, that's fine to do right away.
Receipts are where the tiny writes pile up. Don't store a receipt per message per person. Store one number per person per chat: the highest sequence they've got and the highest they've read. Reading means everything up to there. That's one overwrite, not hundreds of rows.
Send receipts back through a queue and batch them. If someone scrolls through 50 messages, the app only needs to send the last one it read. In big groups, show 'read by 12' instead of pushing every single read to every member.
Step 3: Scaling storage and connections (10 minutes)
Messages are split across lots of machines by chat ID. That's sharding, and it puts a whole chat on one partition, so reading history is one ordered range read. A wide-column store fits this well: it's fast to write and reads rows for one key in order. That's why NoSQL wins for messages, while groups and members stay in a normal database.
Chat servers just hold connections and pass messages along. They don't keep anything that matters, so we can add more. When a phone connects, the server writes 'this user is on me' to the directory with a short expiry, and refreshes it while the connection's alive.
Old history gets read far less. Keep recent months on fast disks and move older chats to cheaper storage. Phones keep a local copy, so scrolling back usually doesn't hit the server at all.
- A really busy chat: one partition takes all its writes. With a 500-member cap, that's still manageable.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| Save before delivering | Deliver first and save in the background | A message the sender saw go through is never lost | We wanted the lowest possible delay and could live with rare losses |
| A sequence number per chat | Timestamps from phones | Everyone sees the same order | Chats got so busy one partition couldn't hand out numbers fast enough |
| One read mark per person per chat | A receipt row per message per person | Way fewer writes and much less storage | Product wanted to show exactly when each message was read |
| Split messages by chat | Split by user | A chat's history is in one place and stays in order | Huge broadcast channels, where one chat gets too hot |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| A chat server dies | Its phones reconnect to another server and catch up from their last sequence numbers |
| A message store partition is slow | Sends to those chats slow down or fail, and the app retries with the same message ID |
| The directory is out of date | A forward goes to the wrong server and fails, so we fall back to push and catch-up |
| Push notifications don't arrive | No message is lost. The user sees it next time they open the app |
| The same message is sent twice | The server sees the message ID again and doesn't save a second copy |
| A big group posts a lot at once | Delivery fans out through the queue, and receipts get batched |
Follow-up questions (5 minutes)
How would you add photos?
Upload the photo to file storage first, then send a normal message that holds a link to it. The chat path stays small.
How do you show who's online?
Use the same connection directory. Only send online and offline changes to people who have that chat open, not to every contact.
What about a user with several devices?
Each device has its own connection and its own last sequence numbers. Deliver to all of them, and sync read marks so reading on one clears the others.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–32 | Sending, ordering and offline delivery |
| 32–40 | Groups and receipts |
| 40–50 | Scaling storage and connections |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Message queues and event streamsKeep services from depending on each other, and handle traffic spikes.
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Sharding and consistent hashingSplit data across machines and keep it balanced.
- Idempotency and retriesMake sure a retried request doesn't happen twice.
- Real-time deliveryPush updates to users the moment they happen, with WebSockets and friends.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Uber.