Design complete systems / CHAPTER 52
Design a messaging service (Facebook Messenger)
Save each intended message once, recover missing messages on offline devices and distinguish saved, delivered and read receipts.
Builds on Design a file synchronization service (Dropbox), Polling, long polling, WebSockets, and server-sent events, Leader and follower replication, Strong versus eventual consistency, Synchronous versus asynchronous communication.
- Distinguish a saved message, an acknowledgment from a recipient device and the app’s report that a message was displayed.
- Assign conversation positions and handle duplicate sends and deliveries.
- Recover missing messages after disconnection without unlimited live buffering.
01Functional requirements
Mira sends one message to Ben. A lost response must not make her retry create a second message, and Ben’s offline phone must later recover the same saved conversation as his laptop. Keep a numbered history so every device can find missing messages.
FR1: Send direct and group messages. Support one-to-one and size-limited group text conversations on web and mobile. Give each send a stable client message ID so retries do not create extra messages.
FR2: Retain and synchronize history. Save accepted messages. Online and returning offline devices can fetch the same conversation history in order, continuing after the last message they saved on that device along with its conversation position.
FR3: Distinguish receipts. Tell the sender when the service has saved a message. Mark it delivered after the recipient device acknowledges saving it. In this example, mark it read when the recipient app reports that the conversation was open and the message displayed. This receipt cannot prove the person actually read or understood it.
FR4: Show temporary presence. Use recent signed-in device activity to estimate whether an account is online. Typing indicators expire; they are not part of saved conversation history.
FR5: Enforce membership and handle attachments. Check current membership before sending messages or reading history. By default, new members can read messages after their join position. Small attachments refer to separately verified private uploads.
Out of scope
- Voice/video calls, unlimited broadcast groups and one global order across all conversations.
- Mandatory end-to-end encryption. Adding it needs an explicit design for keys, recovery and server-side processing.
- Treating presence or a successful socket write as evidence that a person read a message.
Example: Mira sends client message m17 in conversation c8. Ben's laptop receives saved message s81 live; his offline phone later fetches that same message from history.
02Nonfunctional requirements
Assume 100 million daily active users, each sending 40 messages per day, and five million devices connected at once. Confirm these planning assumptions with the interviewer.
NFR1: Send latency. At least 95% of regional sends receive a saved-message acknowledgement within 150 ms at the planned load.
NFR2: Online delivery latency. At least 95% of messages to online recipient devices arrive within 300 ms under normal conditions. Offline delivery and human reading are outside this target.
NFR3: Durable acceptance. A saved-message acknowledgement means the message, its retry result and the work needed for later delivery have been committed. Accepted history survives one storage-node failure under the selected replication policy.
NFR4: One conversation history. Devices eventually display the same per-conversation order. Delivery may repeat, but one successful client message ID produces one visible message. Devices fetch missing messages from saved history.
NFR5: Availability trade-off. Saving and reading history target 99.9% availability per calendar month. If the current writer cannot reach the required database replicas or confirm it still owns the conversation’s next position, leave a send pending or return an error; do not acknowledge two conflicting histories.
NFR6: Current membership. Check current access for sends, history and attachments. Track membership changes with a version so an old connection cannot keep using revoked permission. Default history starts at the member's join position.
NFR7: Bounded connection resources. Limit queued outbound bytes per device. Expire old connection routes and disconnect slow devices with a way to resume from history. A 30-second presence timeout indicates recent activity, not human reading.
NFR8: Honest receipts. Delivered and read positions move forward only after their corresponding application actions. An AI summary does not send messages, change membership or advance receipts.
03Size messages and connections
- **Sends:**100 million users × 40 messages/day = four billion messages/day, about 46,300 sends/second. A fivefold peak is about 230,000/second.
- History storage: at 1 KB for message text and saved fields, four billion messages add 4 TB/day, or 12 TB with three copies before indexes, delivery state and attachments.
- Delivery attempts: one message to 100 members with two connected devices each may require 200 live transmissions. One saved message and 200 device attempts are different workloads.
- Connections: five million devices × 5 KB of assumed application state = 25 GB, excluding transport, operating-system memory and outgoing buffers. Measure full connection cost and simultaneous reconnects before choosing the gateway count.
Decision: size saved history for sends, delivery workers for recipients/devices and connection gateways for their complete measured memory/CPU costs.
04API Design: Requests, Responses and Retries
A send command over HTTP or the socket uses one stable operation identity:
{"type":"send","conversationId":"c8","clientMessageId":"m17","text":"Gate opens at 9","attachmentId":null}
{"type":"persisted","clientMessageId":"m17","messageId":"s81","sequence":81}
Save or retry a message
- An HTTP client sends the same command to
POST /conversations/c8/messages. In the transaction that saves the message and original result, the server checks current membership and assigns the next conversation position. - Return
201 Createdafter that transaction commits. A retry usingm17and the same text returnss81. - Return
409 Conflictifm17is reused with a different conversation, text or attachment;403if membership denies sending;404for a removed conversation. - If the database cannot commit, return
503without claiming success. After an uncertain timeout, retrym17to discover or complete the original operation.
Catch up and report progress
GET /conversations/c8/messages?afterSequence=80&limit=50returns saved messages after position 80 in order, plus a cursor for the next page.- After a device saves all messages through position 81 and records that position, it sends
{"type":"delivered","conversationId":"c8","through":81}. It sends a separate read update after the app displays those messages while the conversation is open. - Save the greater of the previous and received position. A late update for 81 cannot move progress backward from 84, and repeating 81 has no further effect.
GET /conversations?cursor=...reads the account's inbox index to list its conversations. Changing group membership requires a separate authenticated command.
05Data Model and Storage Access Patterns
A conversation's sequence numbers describe its message order. A device cursor records how far that device has progressed; it can differ between the same person's phone and laptop.
| Record and key | Purpose |
|---|---|
Conversation, conversationId |
Current membership version, next sequence and state |
Message, (conversationId,sequence); unique sender/client ID |
Saved text or attachment reference, sender, identity and acceptance time |
Member, (conversationId,accountId) |
Role and the history positions this member may read |
Send result, unique (senderId,clientMessageId) |
Original conversation, text and attachment plus the saved message ID/sequence; compare the command on retry |
Pending delivery, (conversationId,sequence) |
Saved message needing delivery, target progress and retry state |
Device cursor, (accountId,deviceId,conversationId) |
Last message position saved and acknowledged by this device; reconnect after it |
User inbox, (accountId,lastActivity,conversationId) |
List an account's conversations without scanning every message |
Session directory, (accountId,deviceId) |
Which gateway holds the device's connection and when that route expires |
The inbox helps find conversations; the directory helps find connected devices. Neither proves that a message was saved or that an account is still a member. Those decisions use the current conversation records.
06Persist before acknowledging
Start with one conversation application, a transactional history database and periodic client polling.
Send and save
- Mira's client labels one intended send
m17. If a response is lost, it resendsm17rather than inventing a new send ID. - The application checks that Mira currently belongs to conversation
c8, then looks for a saved result for herm17. Matching content returns that result; changed content is rejected. - For a new send, the database locks
c8’s conversation record for the transaction, rechecks current membership and reserves the unique sender/request pair. It reads the next position, 81, advances that counter and saves messages81,m17’s result and pending delivery work together. Another send must wait before reading the updated counter; simultaneous sends cannot both choose 81. Concurrent retries cannot both reservem17. - Return “persisted” only after the configured durable save completes. Here persisted means the service saved the message, not that Ben received or read it.
- A background program finds pending delivery records and retries after crashes. Saving that work with the message closes the gap between “saved” and “scheduled for delivery”; this table is often called an outbox.
Receive and read
- Ben's device asks for conversation messages after its last applied position 80 and receives
s81at 81. - The device saves/applies the message, recognizes repeated
s81IDs and sends a delivered-through 81 acknowledgement. - Read-through 81 means Ben’s app reports that the conversation was open and messages through 81 were displayed. Saving on the server, saving on his device and displaying the messages are separate steps; none proves human understanding.
Each conversation has one accepted sequence; unrelated conversations need no global sequence. If the database cannot save the message with its required replica copies and conversation position, keep it pending on the sender’s device or return an error. Initially delivery work can run alongside the application; separate it when recipient work delays new sends.
BUILD THE DESIGN, ONE CHANGE AT A TIME
Watch the architecture evolve.
Advance through the stages in this same diagram. Existing components keep their places; additions fill the available space, then add rows. Highlights show what changed from the previous stage, and numbered arrows match the connection key below.
Stage 1 of 3
Start here. A useful chat first needs one recoverable meaning for acceptance, replay and receipts.
Stage 1: acceptance belongs to history
Follow the numbered arrows
- Sending device → Conversation serviceSend / retry m17
- Conversation service → History databaseCommit before persisted acknowledgement
- Conversation service → Sending devicePersisted s81 / sequence 81
- Recipient device → Conversation servicePoll after cursor / acknowledge
- Conversation service → Recipient deviceOrdered missing history
Trace the diagram in words
- The conversation transaction assigns position 81 and saves Mira’s m17 as message s81 before replying.
- If Mira retries m17, the service returns the saved s81/81 result rather than appending again.
- Ben’s device fetches missing history, stores one copy of s81 and acknowledges delivery. Read progress changes separately after the app displays it while the conversation is open.
- Deduplicate the stable send ID and assign a conversation sequence in the commit.
- Recipients poll ordered history and send explicit delivered/read progress.
The cost: Polling increases detection delay and request overhead, but keeps live-connection state out of the first implementation.
Why change it? Five million simultaneous devices need connection capacity independent of conversation writes.
Stage 2: sockets carry an application protocol
Follow the numbered arrows
- Sending device → WebSocket gatewaysSubmit / retry stable send ID
- WebSocket gateways → Conversation serviceAuthenticated send / history / receipt
- Conversation service → History databaseDurable ordered history
- Conversation service → WebSocket gatewaysPersisted result / committed delivery
- WebSocket gateways → Recipient deviceLive message / missing history
- Recipient device → WebSocket gatewaysResume cursor / receipt
- WebSocket gateways → Device session directoryRegister current device route
- Conversation service → Device session directoryFind active device destinations
- WebSocket gateways → Sending devicePersisted message ID + sequence
Trace the diagram in words
- A gateway forwards Mira’s m17 command to the service responsible for c8’s saved history.
- The service returns the saved-message result through the gateway; delivery to Ben still needs his device’s acknowledgement.
- After reconnecting, Ben’s device signs in and requests messages after the last position it saved locally.
- Add gateways with limited outbound buffers and a directory that versions each device’s current connection route.
- Forward sends and history-resume requests to the same conversation service that saves ordered messages.
Inspect the connection changes
- Now: Sending device → WebSocket gateways: Submit / retry stable send ID
- Now: WebSocket gateways → Conversation service: Authenticated send / history / receipt
- Now: Conversation service → History database: Durable ordered history
- Now: Conversation service → WebSocket gateways: Persisted result / committed delivery
- Now: WebSocket gateways → Recipient device: Live message / missing history
- Now: Recipient device → WebSocket gateways: Resume cursor / receipt
- Now: WebSocket gateways → Device session directory: Register current device route
- Now: Conversation service → Device session directory: Find active device destinations
- Now: WebSocket gateways → Sending device: Persisted message ID + sequence
- Previously: Sending device → Conversation service: Send / retry m17
- Previously: Conversation service → History database: Commit before persisted acknowledgement
- Previously: Conversation service → Sending device: Persisted s81 / sequence 81
- Previously: Recipient device → Conversation service: Poll after cursor / acknowledge
- Previously: Conversation service → Recipient device: Ordered missing history
All other connections are retained. Response paths are implicit unless drawn.
The cost: Gateways add heartbeats, reconnect storms and expiring routes; stale or missing routes must recover through durable history.
Why change it? One group send can produce 200 device attempts; slow recipients must not stall ordered message acceptance.
Stage 3: durable intent drives independent delivery
Follow the numbered arrows
- Sending device → WebSocket gatewaysSubmit / retry stable send ID
- WebSocket gateways → Conversation serviceAuthenticated send / history / receipt
- Conversation service → History databaseSaved ordered messages and delivery work
- Conversation service → WebSocket gatewaysPersisted send / history result
- WebSocket gateways → Recipient deviceLive message / missing history
- Recipient device → WebSocket gatewaysResume cursor / receipt
- WebSocket gateways → Device session directoryRegister current device route
- Delivery workers → History databaseClaim intent / load committed message
- Delivery workers → Device session directoryResolve current device routes
- Delivery workers → WebSocket gatewaysRetry committed message per device
- WebSocket gateways → Sending devicePersisted message ID + sequence
Trace the diagram in words
- The conversation database commits the message, original send result and pending delivery record together.
- Delivery workers find each permitted recipient device’s current gateway and retry the saved message.
- A gateway disconnects a device whose outgoing buffer reaches its limit; the device later retrieves s81 from saved history.
- Run delivery attempts in separate workers that recover unfinished work from the saved outbox.
- Keep message commits and history catch-up reads in the original conversation store.
Inspect the connection changes
- Now: Conversation service → History database: Saved ordered messages and delivery work
- Now: Conversation service → WebSocket gateways: Persisted send / history result
- Now: Delivery workers → History database: Claim intent / load committed message
- Now: Delivery workers → Device session directory: Resolve current device routes
- Now: Delivery workers → WebSocket gateways: Retry committed message per device
- Previously: Conversation service → History database: Durable ordered history
- Previously: Conversation service → WebSocket gateways: Persisted result / committed delivery
- Previously: Conversation service → Device session directory: Find active device destinations
All other connections are retained. Response paths are implicit unless drawn.
The cost: Workers must save delivery progress, recognize repeats and measure waiting time. Live-route failures still do not remove accepted messages from history.
07Separate connection routing, delivery and presence
Choose how devices communicate
- Periodic polling repeatedly asks for new messages: simple, with extra requests or delay.
- Long polling holds a request until an update or timeout, then repeats.
- A WebSocket keeps a two-way connection open. It reduces repeated setup but does not save messages or recover offline history by itself.
Find Ben's devices
- His phone connects to gateway
G1; his laptop connects toG2. A gateway is the server holding that device's live connection. - A session directory saves each device's gateway and an expiry time. A new session version on reconnect prevents a delayed old connection update from replacing its newer route.
- A delivery worker reads these routes and sends the saved message to both devices. If the directory points to a gateway that no longer holds the device’s connection, that delivery attempt fails. On reconnect, the device fetches missing messages from saved history.
- Limit queued outgoing bytes. Disconnect a device that cannot keep up and let it resume from its last applied position, instead of retaining unlimited memory.
Estimate online status
- Devices send periodic “still connected” heartbeats. In this example, expire presence after 30 seconds without one.
- Ben can remain online while either approved device has recent activity. Typing indicators expire even sooner.
- Presence estimates recent connection activity. It proves neither delivery nor reading.
Choose storage placement
- Measure gateway CPU/memory, then add gateways and a shared route directory when needed.
- Partition history by conversation to keep its ordered messages together; use a separate user-inbox index to list conversations.
- Storing group history separately for each user duplicates content and complicates one order.
- A queue can route many conversations by key through a limited number of partitions; it need not create a physical topic for every chat.
An accepted message survives duplicate delivery and a missing socket
Follow the numbered arrows
- m17 submitted twice → c8 historyFind saved send result before inserting
- c8 history → Device session routesSaved message-delivery work
- Device session routes → Online laptopMay deliver more than once
- Offline phone → History catch-upReconnect with cursor
- c8 history → History catch-upRead missing messages from history
Trace the diagram in words
- Repeated m17 maps to one accepted message.
- Each current device has a separate delivery route.
- An offline device recovers from history rather than gateway memory.
08Separate live gateways from durable ordering
- Separate connections from saving. Gateways hold live device connections; conversation services save messages. The directory tells delivery programs which gateway to contact.
- Partition by conversation. Give each conversation one ordering mechanism and replicate its accepted history under the durability promise before acknowledging saves.
- Replace a failed writer safely. Losing contact does not prove the old writer stopped. For example, storage records that writer permission 12 has been replaced by 13. Each write compares its permission number with that saved number as part of accepting the write; a late write carrying 12 is rejected. A separate earlier comparison could race replacement. This rejection is called fencing.
- Recover delivery work. Workers read the saved outbox, retry unfinished sends and mark completed work. Clients check message IDs so repeated delivery does not add another visible message.
- Limit live buffering. Expire old routes and disconnect slow devices with a history-resume position. Accepted messages remain in saved history even when their connection disappears.
09Follow an online send and an offline return
- The response is lost: Mira sends
m17. The transaction savess81at position 81 plus the original result and pending delivery work, but Mira receives no acknowledgement. - The send is retried: Mira resends
m17. The application finds its saved result and returns s81/81. It does not append another message. - A live device receives it: a delivery program finds Ben's laptop route. The laptop checks whether
s81is already stored; it saves one visible copy, then acknowledges delivered-through 81. - An offline device catches up: Ben's phone last applied 75. It fetches 76–81 from saved conversation history. If live 82 arrives first, keep it waiting while fetching the gap, then apply the ordered messages.
- Ben reads: read-through advances when his app reports the conversation open and messages displayed through that position. Receiving bytes alone does not advance it.
- Access changes: attachment downloads and history reads recheck current membership. Removing Ben updates the membership version; an old signed-in socket cannot preserve the old permission.
For groups, many delivery attempts still refer to one saved message. Stable send IDs prevent duplicate creation; stable message IDs prevent duplicate display. They solve different retry points.
10State privacy, overload and ordering limits
- A region cannot reach the replicas needed to save a send: leave it pending or return an error. Letting disconnected writers each confirm a different message at position 81 would break the single conversation order promised here.
- A group is very busy: batch database writes and separate recipient delivery first. Splitting one conversation's ordered writes is a larger correctness change, not just adding a server.
- A writer is replaced: reject the old writer at the accepted write path as described above. A timeout alone is insufficient.
- A device falls behind: bound its outgoing queue and recover from saved history after reconnecting. Measure oldest pending delivery age as well as queue length.
- Security: encrypt transport/storage, revoke devices, check membership and rate-limit abuse. End-to-end encryption is a separate design where only endpoints hold message keys; it changes recovery, search and review.
- Observe cost and failures: track save latency, delivery delay, reconnect bursts and reuse of one send ID with changed content. Retained history drives storage; recipients and devices multiply delivery work.
OPTIONAL DESIGN EXTENSIONWhere AI Fits in This Design
Use: summarize an allowed conversation range
- Input: Ben selects messages he may currently read. Bound the range rather than sending his entire chat history.
- Output: a draft catch-up summary with links to the actual source messages. Saved history remains available to check exact wording.
- Integration: authorize first, then summarize outside message-save and live-delivery work. A cached summary records its source range/versions and must recheck permission before return.
- Limits on action: generating or viewing it does not send a reply, change membership or automatically advance delivered/read receipts. Remove or invalidate it when sources are deleted or access changes.
- Fallback: return original permitted history without a summary when inference fails.
- Evaluation and cost: test omitted decisions, invented commitments, wrong source links and user corrections. Cap messages, tokens, retries and a proposed two-second response deadline. Agree whether private messages may leave the chat service, restrict the processing service’s access and keep message text out of routine logs. With end-to-end encryption, the server cannot read messages for a summary; run the model on the client or obtain the user’s consent to send selected decrypted messages to the model service.
AWS's summarization evaluation guidance describes useful quality dimensions, not a guarantee of accurate history.
DRAW. CALCULATE. EXPLAIN.
Explain a send across acknowledgement loss and failover
75 minutes · guided practice
Mira sends request m17 to conversation c8. The database saves message s81 at position 81, but its acknowledgment is lost. The writer fails, and Mira retries. Ben’s laptop is online and has received through position 80; his phone is offline and has received through 75. Position 82 arrives live while the phone is catching up.
- Show writer replacement and how retrying
m17returns the originals81. - Draw how each device catches up without displaying 82 before missing earlier messages.
- Keep separate positions for saved, delivered and read progress.
- Calculate live delivery attempts for one message to 100 members with two connected devices each.
Compare the worked solution
- Let a replacement recover the committed message history. Prevent the old writer from making accepted writes before the replacement starts accepting new sends.
- Find Mira’s saved result for intended send
m17: saved messages81at conversation position 81. Return that result instead of adding another message. - The delivery program looks up Ben’s current devices and retries
s81. Each device checks its stored message IDs and keeps only one visibles81. - His laptop can acknowledge 81 after applying it. His phone, last through 75, fetches 76–81 before applying an early live 82.
- Keep the highest delivered position and advance read-through only after Ben’s explicit read action. Connection presence does not prove either action.
- One group message × 100 members × two devices can produce 200 live attempts, while still being one history entry. Limit gateway queues and let disconnected devices resume from history.
The checkboxes record your own assessment. The worked solution gives you a way to check the reasoning.
RETRIEVE BEFORE YOU REVEAL
Keep the important ideas.
Say the answer aloud, then turn the card over.
CARD 01What makes messaging retry-safe?
- Save one result for each sender’s intended send ID.
- Return that result on retry.
- Receivers store one visible copy per saved message ID.
- Fetch missing conversation positions from history.
CARD 02How do receipt and presence guarantees differ?
- Persisted: the service saved the message.
- Delivered: a recipient device acknowledged saving it and its position.
- Read: the recipient app reported the conversation open and messages displayed. This does not prove human understanding.
- Presence: a device recently reported connection activity.
CARD 03Why separate conversation logs from device routes?
- History stores accepted messages for offline recovery.
- The directory locates live devices; routes can expire or be wrong.
- The inbox lists conversations.
- Current membership checks still govern history, attachments and optional AI summaries.
CHECK THE REASONING
Can you explain the choice?
Answer every question, then finish for an immediate score and an explanation for each answer. Aim for 80%, then revisit any gaps.
CLOSE THE LOOP
Explain it without the page.
Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.