A chat app sounds easy — "send a message from A to B." The tricky part is the receiving side. The web's normal rule is that the client always starts the conversation; the server only answers. So how does the server push a new message to someone who didn't ask for it right now? Get that one idea right — plus where you store messages and how you show who is online — and you have a design like Facebook Messenger that handles 1-on-1 chat, small groups, and an online dot for 50 million daily users.
The animation on the right follows one message from start to finish. First a heartbeat keeps User A marked as online. Then her message gets a unique ID, goes onto a message queue, is saved in the database, and is finally either pushed to User B over a live connection (if B is online) or handed to the push-notification server (if B is offline).
Step 1: Decide what we are building
Agree on the exact chat type before you draw anything. Designing for group chat when the interviewer wanted 1-on-1 is a common mistake. For a Messenger-style app, here is the agreed list of features:
- 1-on-1 chat that feels fast (low delay)
- Small group chat — up to 100 people
- An online indicator (the green dot)
- Multiple devices — the same account logged in on phone and laptop at once
- Push notifications (alerts when the app is closed)
- Text only, under 100,000 characters per message; chat history is kept forever
Back-of-the-envelope estimation
These rough numbers explain every choice we make later. ("Back-of-the-envelope" just means quick math, not exact.)
Daily active users = 50,000,000 DAU
Connections at once ≈ 1,000,000 (peak, rough)
Memory per connection ≈ 10 KB
Memory for all of them = 1,000,000 × 10 KB ≈ 10 GB on one box (in theory)
Messages per day (industry) ≈ 60 billion (Messenger + WhatsApp)
Reads vs writes (1-on-1) ≈ 1:1
That 10 GB number is a trap, not a goal. Yes, one machine could physically hold all the connections. But putting everything on one server is a bad idea — if that one box goes down, the whole app goes dark. Say the number out loud, then reject it right away.
Start with the connection-count math, then say plainly: "this fits on one server, which is exactly why I won't do it — it is a single point of failure." Stating the number and why it is a red flag in the same breath is a strong senior signal.
Step 2: How do clients and servers talk?
Clients never message each other directly. Each one connects to a chat service in the middle that passes messages along. Sending is the easy half — a normal request from the client works fine. The hard half is receiving: the server cannot start an HTTP request on its own, so how does a brand-new message reach a client that is just sitting there?
| Technique | How it works | Why it is not great |
|---|---|---|
| Polling | Client keeps asking "any messages?" over and over | Wasteful — most answers are "no"; the more often it asks, the more it costs |
| Long polling | Client asks and the server holds the request open until a message arrives or it times out | Sender and receiver may land on different servers; the server can't tell when a client quietly disconnects; it still has to reconnect after every timeout |
| WebSocket | An HTTP connection gets upgraded into a permanent two-way channel | The winner — the server can push a message anytime |
WebSocket
A connection that the client starts, works both directions, and stays open. It begins as normal HTTP and is then "upgraded" with a quick handshake. It slips through firewalls because it uses the usual web ports (80 and 443). Once it is open, the server can push to the client whenever it wants — which is exactly what delivering messages and showing presence need.
Because WebSocket works both ways, we use it for sending too. Using one channel for both sending and receiving keeps the client and server simpler. Everything else (sign-up, login, profile) stays as plain request-and-response over HTTP. WebSocket is only for the real-time part.
Interviewers want you to walk through polling → long polling → WebSocket and explain why each step exists. Don't just name-drop WebSocket. The senior move is to explain what breaks with long polling (stateless servers, no way to detect a disconnect) and how that pushes you to WebSocket.
Step 3: High-level design
Sort the system into three groups:
- Stateless services — login, signup, profile, and service discovery (finding the right server). These keep no per-user memory, sit behind a load balancer, and are easy to scale.
- Stateful service — the chat servers. They are stateful because each client pins one WebSocket to one server and stays on that server the whole time it is connected.
- Third-party integration — push notifications (covered in the notification-system chapter).
┌──────────────┐
Client ──WS──▶│ Chat Servers │──▶ ID Generator (message_id)
└──────────────┘──▶ Message Sync Queue ──▶ KV Store (history)
Client ──────▶ API Servers (login/signup/profile) │
Client ──WS──▶ Presence Servers (online/offline) └─▶ Chat Server 2 ──WS──▶ recipient
Notification Servers (push) ◀───────────────── (if offline)
Chat servers handle real-time messages. Presence servers track who is online or offline. API servers do everything else. Notification servers send alerts. The key-value store (a simple database that looks things up by key) saves chat history, so a user who was offline sees her full history when she comes back.
Storage: why a key-value store
Two kinds of data live here. General data (profiles, settings, friend lists) goes in a normal relational database (copied and split across machines for safety and scale). Chat history is the interesting one, and how it is used points us to a key-value store:
- A huge volume (~60 billion messages a day across the industry)
- Mostly only recent chats are read, but search and "jump to" need to grab any message at random
- Reads and writes happen about equally (≈ 1:1)
- Relational database indexes get slow over a giant pile of old data, while key-value stores stay fast and add machines easily
Real products prove it works: Messenger uses HBase, Discord uses Cassandra.
Message IDs
Every message needs an ID that sets its order, so the ID must be unique and sortable by time (newer messages get bigger IDs). You can't rely on a created_at timestamp, because two messages can land on the exact same time.
| Approach | Verdict |
|---|---|
MySQL auto_increment | Key-value stores don't offer this |
| Snowflake (global 64-bit) | Works; unique and time-sortable across many machines |
| Local sequence (counts up per channel) | Simplest — you only need correct order inside one channel |
At the ID step, say: "ordering only has to be correct within a channel, so a simple local counter is enough — I don't need global Snowflake IDs here." Picking the cheaper option that is still good enough, over the fancy one, shows judgment.
Step 4: Deep dive
Service discovery
Its job: when a client logs in, hand it the best chat server to use (based on location, how busy each server is, and so on). A tool like Apache ZooKeeper keeps a list of all chat servers and picks one:
- User A logs in; the load balancer sends her to an API server.
- The API server checks who she is, then asks service discovery for the best chat server.
- Service discovery answers with, say, Chat Server 2.
- User A opens a WebSocket to Chat Server 2.
1-on-1 message flow
This is the path the animation walks through:
- User A sends the message to Chat Server 1.
- Chat Server 1 gets a message_id from the ID generator.
- It drops the message onto the message-sync queue (the recipient's inbox).
- The message is saved in the KV store.
- (a) If User B is online, send it on to Chat Server 2 (where B is connected); (b) if offline, fire a push notification.
- Chat Server 2 pushes the message to User B over her open WebSocket.
Multi-device sync
Each device remembers a cur_max_message_id — the latest message ID it has already seen. A message is new for a device when it is addressed to that user and its ID in the KV store is bigger than that device's cur_max_message_id. So a phone and a laptop on the same account each pull exactly what they missed, on their own.
Small group chat
When you send to a group, the message is copied into each member's inbox (B's inbox, C's inbox, and so on). Then each person only has to check their own inbox — simple, and cheap while groups are small. This "copy on send" works fine up to ~100 people (WeChat caps groups at 500). For very large groups, copying once per member gets too expensive.
Online presence and the heartbeat
The naive plan — "mark offline the moment the connection drops, online when it comes back" — flickers badly. Picture a phone going through a tunnel. The fix is a heartbeat.
Presence heartbeat
An online client sends a small "I'm still here" message to the presence servers on a timer (say every 5 seconds). The user stays online as long as one of these arrives inside a time window (say x = 30 seconds). Miss the window — for example, three heartbeats then a drop with no reconnect — and presence flips to offline. This soaks up short network blips instead of flipping the green dot on and off.
To tell friends about a status change, the system uses publish/subscribe (one writer announces; many readers listen). Each pair of friends shares a channel, so when A's status changes she announces it on her channels A-B, A-C, A-D, and those friends get the update over WebSocket. This is fine for short friend lists. A 100,000-member group would fire 100,000 updates per change, so for big groups you instead fetch status only when needed — when a user opens the group or refreshes the list.
The heartbeat is the highest-signal part of this design. Bring up the failure first ("naive disconnect handling makes the dot flicker on shaky networks"), then introduce the heartbeat with a real interval and timeout. Naming the problem before the solution is what separates senior answers.
Wrap-up
The chat system boils down to a few parts, each with a clear job:
| Component | Responsibility |
|---|---|
| Chat servers | Send and receive in real time over WebSocket |
| Presence servers | Track online/offline using heartbeat + pub/sub |
| Message sync queue | One inbox per recipient; splits saving from delivery |
| KV store | Chat history, kept forever (HBase / Cassandra) |
| API servers | Login, signup, profile (plain HTTP) |
| Notification servers | Push an alert when the recipient is offline |
If you have time left, the natural add-ons are media files (compress them, store in the cloud, make thumbnails), end-to-end encryption (WhatsApp), caching messages on the client, edge caches near users (Slack's Flannel), and error handling — if a chat server dies, service discovery hands clients a new one, and a retry/queue system resends any dropped messages.
A chat system in 45 minutes: 5 min on requirements + the connection-count math → 10 min on the protocol choice (polling → long polling → WebSocket) and the high-level diagram → 25 min on deep dives (1-on-1 flow, KV storage, presence heartbeat) → 5 min wrap-up and extensions. Spend the most time on the message flow and presence — that is where the interview is won.
Practice
Check that the message flow and the receiver problem make sense.
1. Why is the receiving side of a chat app the hard part?
2. What is a WebSocket?
3. Why store chat history in a key-value store instead of a relational database?
4. What problem does the presence heartbeat solve?
5. If User B is offline when a message is sent, what happens?