Worked Example: Designing a Chat System (like WhatsApp)
You'll learn to
- -Apply the design loop to a stateful, real-time system
- -See why a persistent connection breaks the statelessness assumption from Module 2, and how to work around it
The URL shortener was almost entirely stateless request/response. A chat system breaks that assumption immediately: it needs a persistent, long-lived connection per user, which directly conflicts with the "any server can handle any request" principle from Module 2. This chapter is about resolving that tension.
Step 1: Clarify Requirements
- -Functional: 1:1 and group messaging, delivery status (sent/delivered/read), online presence, message history that syncs across a user's devices.
- -Out of scope: media/file transfer internals (assume object storage handles that, covered in Module 11), voice/video calls.
- -Non-functional: messages must arrive in the right order within a conversation, delivery should feel near-instant when both users are online, and no message should be silently lost if a user is briefly offline.
Step 2: Estimate Scale
That last row is the number that makes this system architecturally different from anything earlier in the course: it is not just about handling requests, it is about holding open millions of simultaneous, stateful connections.
Step 3: High-Level Design
Sender and recipient may be connected to different gateway servers: a presence registry tells the message service where to route each message.
The message store is deliberately a wide-column NoSQL database, not a relational one: the access pattern is almost always "give me messages for conversation X, ordered by time," a simple, massive-write-volume pattern that plays directly to a wide-column store's strengths, exactly as the Databases 101 module framed the choice.
Step 4: Deep Dive - The Stateful Connection Problem
A user's WebSocket connection is pinned to one specific gateway server, which is unavoidable, since a TCP connection cannot be load-balanced mid-flight to a different machine. This means any OTHER server that needs to deliver a message to that user must first discover which gateway currently holds their connection. The fix: a presence registry (a fast key-value store, mapping user_id -> gateway_server_id) that every gateway updates on connect/disconnect, and that the message service consults before routing every message.
- -User comes online: their gateway writes {user_id: gateway_id} to the presence registry.
- -A message arrives for them: the message service looks up their current gateway, then forwards the message there specifically.
- -User goes offline or the gateway crashes: the registry entry is removed (or expires via TTL), and the message service instead writes the message to a durable per-user inbox for delivery on reconnect.
Ordering and Delivery Guarantees
Two different servers' clocks can disagree, so a raw timestamp is not a safe way to order messages within a conversation. Instead, assign a per-conversation monotonic sequence number at write time: every client can then reliably reconstruct "what came after what," independent of clock skew. For delivery, the system guarantees at-least-once (the same guarantee the Async Systems module named as the practical default for queues); a client may occasionally receive a duplicate after a reconnect, and de-duplicates using the message's unique ID, exactly the idempotency pattern from Module 12.
Step 5: Trade-offs & Failure Modes
- -Group chat fan-out: for a group of N members, the message service pushes to N recipients' gateways per message. Unlike a social feed, N here is small (tens to low hundreds), so simple fan-out-on-write is fine: no celebrity problem at this scale (the next chapter is the case where that stops being true).
- -Consistency: "delivered" and "read" receipts propagate asynchronously and can briefly disagree across a user's own devices, which is acceptable, since nobody expects perfectly synchronized read receipts within milliseconds.
- -Failure mode: a gateway server crashes. Every connection it held drops. Clients detect the dropped socket, reconnect (likely to a different gateway via the load balancer), re-register their presence, and request "everything since sequence number N" for each conversation to fill the gap: the sequence number from the deep-dive is what makes this resync possible at all.
Why can't you just load-balance WebSocket connections the same way you load-balance stateless HTTP requests?
"WebSockets are just a different protocol, so you need a different load balancer."
"A WebSocket is a single long-lived TCP connection: once established, it is physically pinned to one specific server process holding that socket. A stateless HTTP load balancer picks a server per-request; here, the choice is made once, at connect time, and every server for the life of that connection must be reachable through that same pinned path. That is exactly why a separate presence registry is needed: it is the piece that lets the rest of the stateless system find a fundamentally stateful connection."
Why does a chat system need a "presence registry" at all?
Level 11: Chat System (WhatsApp) in the HLD Lab is exactly this system - go build the design you just walked through.