High-Level Design and the Acknowledgement Chain
In one line: the ordering of two steps here — store before confirming delivery — is what makes the system reliable, and it is easy to read past.
The flow
| Step | What happens |
|---|---|
| 1 | User A and User B establish connections with the chat server |
| 2 | User A sends a message to the chat server |
| 3 | The chat server acknowledges receipt to User A |
| 4 | The server attempts delivery to User B and stores the message to ensure delivery if the receiver is offline |
| 5 | User B sends an acknowledgment to the chat server |
| 6 | The server notifies User A that the message was delivered |
| 7 | When User B reads the message, the application notifies the server |
| 8 | The server notifies User A that the message has been read |
Step 4 stores and delivers together — and storing must come first
The step reads "attempts to deliver the message to User B and stores it in the database." The conjunction hides an ordering decision that determines whether the system loses messages.
Consider the two orders:
DELIVER, then STORE: server crashes between them
-> B never got it, and it was never stored
-> MESSAGE LOST
STORE, then DELIVER: server crashes between them
-> message is durable, redelivered on recovery
-> message DUPLICATED at worst
Store first. Losing a message is unrecoverable; duplicating one is a deduplication problem the client can solve with the message_ID from Lesson 5's API.
This is the same principle every chapter in this module has reached from a different direction — acknowledge on durability, propagate afterwards — and it is why step 3 comes before step 4. The server tells A "I have it" the moment it is safe, not when B receives it.
Note what that means for the sent tick: it confirms server receipt, not delivery. That is exactly the right guarantee to expose, because it is the only one the server can make immediately, and it is why there are three tick states rather than one.
In any store-and-forward system, durability precedes forwarding, and the first acknowledgement means "durable," not "delivered."
The three states are three different parties confirming three different things
Trace who originates each acknowledgement:
| Tick | Originated by | Confirms |
|---|---|---|
| Sent (step 3) | The server | The message is durable |
| Delivered (steps 5–6) | B's device | The device has it |
| Read (steps 7–8) | B's app | A human saw it |
Each requires a message travelling back to A, which is Lesson 3's point about the real event rate being roughly three times the message rate.
Two design observations.
Each state is progressively less about the system and more about the human. Sent is infrastructure, delivered is a device, read is a person. Which is why read receipts are the one users can disable — it is the only tick that discloses behaviour.
Delivered and read both require the recipient's device to be online, so both can lag arbitrarily. A message can sit in sent for days if B never reconnects, which is exactly the case the 30-day retention in Lesson 3 exists to bound.
The acknowledgement chain is a distributed state machine whose transitions are driven by a party you do not control.
The inbox: how offline delivery actually works
Live delivery only works if the recipient is connected. Everything else depends on a per-recipient queue of things they have not yet acknowledged.
Three properties worth stating explicitly.
The message is stored once; the inbox entry is per recipient. Content lives in one row; what varies per person is whether they have received it yet.
Deletion is driven by acknowledgement, not by sending. The entry survives until the client confirms receipt, which is what makes delivery survive a crash on either side.
Entries carry a TTL. Undelivered messages cannot accumulate forever, so retention is bounded — typically around thirty days — and that bound is a product decision with a direct storage consequence.
The same structure handles multiple devices: the inbox is keyed per client, not per user, so a message is delivered independently to a phone, a tablet, and a laptop, each acknowledging on its own schedule.
The API
sendMessage(message_ID, sender_ID, receiver_ID, type,
text=none, media_object=none, document=none)
getMessage(user_Id)
uploadFile(file_type, file)
downloadFile(user_id, file_id)
A client-supplied message_ID is what makes retries safe
sendMessage takes a message_ID as its first parameter — supplied by the client, not generated by the server.
That is the idempotency key the TinyURL and Uber chapters both wanted and did not have. Its purpose here is specific:
Lesson 4's store-then-deliver ordering means crashes cause duplicates, not losses. A client-supplied ID lets the recipient discard the second copy, and lets the sender retry safely after a timeout without creating a second message.
Send times out -> client retries with the SAME message_ID
-> server recognizes it, does not duplicate
Without a client-supplied ID, a retry is indistinguishable from a new message. On an unreliable mobile network — which is the whole operating environment here — retries are constant, so this parameter is doing more work than any other in the API.
Note also that user IDs are phone numbers, per the design. Which quietly means the system has no separate identity layer: the phone network's namespace is the app's namespace, with all the consequences that carries for account recovery and for privacy.
getMessage(user_Id) takes no cursor and returns everything
This API retrieves all unread messages when a user comes online after a period of inactivity.
Two problems, both familiar from earlier chapters.
No pagination. A user offline for three weeks may have thousands of messages across dozens of conversations. Returning all of them in one response is a large payload on a mobile connection that has just reconnected — which is precisely when it is least reliable.
No cursor, so no resumability. If the response fails halfway, the client starts over. It cannot say "I have everything up to message X."
The fix is the same one the Twitter and newsfeed chapters needed: a cursor, and here it is easier than it was there, because messages have a natural order. A monotonic message sequence per conversation gives a stable cursor with no ambiguity.
A bulk-fetch API called exactly when the network is least reliable is the one that most needs resumability.
Media has its own API pair, and that separation is deliberate
uploadFile and downloadFile are separate from sendMessage, and the design gives the limits: 16 MB for media, 100 MB for documents.
Compare a message at 100 bytes. A document is a million times larger, which is why Lesson 8 gives media an entirely separate service rather than pushing it through the WebSocket path.
The flow that results is worth naming now: upload the file, get an ID, send the ID as a message. The heavy payload never touches the messaging path; only a reference does.
Fan out references, not values — the same rule every chapter in this module has arrived at, applied here to keep megabyte payloads off a connection tier tuned for millions of tiny sockets.
Key takeaway
Step 4's "deliver and store" conceals an ordering that determines correctness: store first, because a crash then duplicates rather than loses — and duplication is solvable with the client's message_ID while loss is not. The first acknowledgement means "durable," not "delivered," which is why three tick states exist. Each state is confirmed by a different party — server, device, human — and each is progressively more about the person, which is why only read is disableable. The client-supplied message_ID is the idempotency key that makes retries safe on the unreliable networks this system lives on. And getMessage returns everything with no cursor, called at exactly the moment the network is least reliable.
Next: WebSocket connections and the manager that finds people.