The System Design Interview: A Step-by-Step Framework
A step-by-step framework for system design interviews: clarifying requirements, estimates, APIs, data models, high-level design, deep dives and trade-offs.
How to design a chat app: WebSockets, message flow and ordering, storage, group chat fan-out, presence, offline delivery and multi-device sync.

A chat system needs three things working together: persistent connections so messages arrive instantly, a durable message store that fixes the order of each conversation, and a routing layer that delivers each message to every device of every recipient, online or not. The classic design uses WebSockets to stateful chat servers, a message service that assigns per-conversation sequence numbers, a pub/sub layer for fan-out and push notifications for anyone offline.
Functional: one-to-one and group chat, message history, delivery and read receipts, online presence, push notifications, and sync across a user’s phone, laptop and browser.
Non-functional: low latency (a message should feel instant), no lost messages, a consistent order within each conversation, and high availability. Scope it early: are groups capped at a few hundred members, or can channels hold hundreds of thousands? That single choice changes the fan-out design.
As in any interview, agree requirements and scale before drawing boxes; our system design interview framework walks through that opening.
Suppose 50 million daily active users each sending 40 messages a day: 2 billion messages a day, about 23,000 per second on average and perhaps three times that at peak. At around 200 bytes per message with metadata, that’s roughly 400 GB of new data a day before replication. Our guide to back-of-the-envelope estimation shows how to reach numbers like these quickly.
HTTP request-response is a poor fit for messages that arrive unprompted. The usual answer is a WebSocket (RFC 6455): a full-duplex connection that stays open, so the server can push a message the moment it arrives. Long polling and server-sent events are fallbacks.
Connections make chat servers stateful: the server holding a user’s socket is the only one that can push to that device. You need:
Put a layer-4 or WebSocket-aware load balancer in front, spreading connections evenly across chat servers.
The client-generated ID makes retries safe: if the sender resends after a timeout, the store recognises the duplicate. This is the same idea as idempotency keys for APIs.
Global ordering across all chats is unnecessary and expensive. What users notice is order within a conversation, so give each conversation its own monotonically increasing sequence number, assigned by whichever node owns that conversation. Timestamps alone aren’t enough, because clocks on different servers drift.
Messages are written constantly, read mostly by recency and rarely updated, which suits a wide-column or key-value store partitioned by conversation ID and sorted by sequence number. Fetching “the last 50 messages” becomes one partition read. User profiles, contacts and group membership fit a relational database. As volume grows, shard messages by conversation ID so each conversation lives on one partition.
| Data | Store | Access pattern |
|---|---|---|
| Messages | Wide-column / key-value | Append; read latest by conversation |
| Users, groups, membership | Relational | Point lookups, joins |
| Connection registry, presence | In-memory key-value | Very frequent reads and writes; short TTLs |
| Media (images, video) | Object storage + CDN | Upload once, read many |
For small groups, fan out on write: look up the members and publish to each one’s devices. For very large channels, that becomes millions of deliveries per message, so switch to fan out on read: store the message once and let clients fetch new messages for the channel when they open it, with pushes only for mentions. The same trade-off appears in news feed design.
Use TLS everywhere. Many messaging apps also offer end-to-end encryption, where only the participants’ devices hold the keys; the Signal Protocol is the best-known design. End-to-end encryption changes the architecture: servers route and store ciphertext, and features like server-side search must move to the device.
Polling wastes requests when nothing has changed and adds delay when something has. WebSockets keep one connection open so the server can push messages instantly.
Assign a sequence number per conversation when the message is stored, and have clients display messages by that number rather than by timestamps.
Clients reconnect to another server, which updates the connection registry. Because messages were acknowledged only after being stored, each device then syncs anything newer than its last sequence number.
Every article is edited by a human and checked against our editorial policy. Spotted a mistake? Tell us.
A step-by-step framework for system design interviews: clarifying requirements, estimates, APIs, data models, high-level design, deep dives and trade-offs.
A URL shortener system design: requirements, capacity estimates, API, short-code generation, database choice, caching, analytics and scaling trade-offs.
Microservices or a monolith? The real costs and benefits of each, the modular monolith middle ground, and a practical framework for deciding and migrating.