LearnastraTHE DESIGN COURSE

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.

Advanced3h 20m of deliberate study10 questions

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.

By the end, you can
  • 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.
INTERVIEW STEP 1 / 7Clarify the requirements

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  1. NFR1: Send latency. At least 95% of regional sends receive a saved-message acknowledgement within 150 ms at the planned load.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

INTERVIEW STEP 2 / 7Estimate the scale

03Size messages and connections

  1. **Sends:**100 million users × 40 messages/day = four billion messages/day, about 46,300 sends/second. A fivefold peak is about 230,000/second.
  2. 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.
  3. 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.
  4. 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.

INTERVIEW STEP 3 / 7Define the API

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

  1. 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.
  2. Return 201 Created after that transaction commits. A retry using m17 and the same text returns s81.
  3. Return 409 Conflict if m17 is reused with a different conversation, text or attachment; 403 if membership denies sending; 404 for a removed conversation.
  4. If the database cannot commit, return 503 without claiming success. After an uncertain timeout, retry m17 to discover or complete the original operation.

Catch up and report progress

  1. GET /conversations/c8/messages?afterSequence=80&limit=50 returns saved messages after position 80 in order, plus a cursor for the next page.
  2. 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.
  3. 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.
  4. GET /conversations?cursor=... reads the account's inbox index to list its conversations. Changing group membership requires a separate authenticated command.
INTERVIEW STEP 4 / 7Define the data model

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.

INTERVIEW STEP 5 / 7Draw the high-level design

06Persist before acknowledging

Start with one conversation application, a transactional history database and periodic client polling.

Send and save

  1. Mira's client labels one intended send m17. If a response is lost, it resends m17 rather than inventing a new send ID.
  2. The application checks that Mira currently belongs to conversation c8, then looks for a saved result for her m17. Matching content returns that result; changed content is rejected.
  3. 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 message s81, 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 reserve m17.
  4. Return “persisted” only after the configured durable save completes. Here persisted means the service saved the message, not that Ben received or read it.
  5. 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

  1. Ben's device asks for conversation messages after its last applied position 80 and receives s81 at 81.
  2. The device saves/applies the message, recognizes repeated s81 IDs and sends a delivered-through 81 acknowledgement.
  3. 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

RetainedAdded / new routeChanged responsibility / route

Start here. A useful chat first needs one recoverable meaning for acceptance, replay and receipts.

Stage 1: acceptance belongs to history

Stage 1: acceptance belongs to historyThe 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. Numbered arrows correspond to the connection key below. All connections are directional.Sending deviceStable client messageIDConversationserviceCheck members; savemessages in orderHistory databaseMessages, saved sendresults and workRecipient deviceLast appliedposition;delivery/read status12345Stage 1: acceptance belongs to historyThe 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. Numbered arrows correspond to the connection key below. All connections are directional.Sending deviceStable client messageIDConversationserviceCheck members; savemessages in orderHistory databaseMessages, saved sendresults and workRecipient deviceLast appliedposition;delivery/read status12345Stage 1: acceptance belongs to historyThe 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. Numbered arrows correspond to the connection key below. All connections are directional.Sending deviceStable client messageIDConversationserviceCheck members; savemessages in orderHistory databaseMessages, saved sendresults and workRecipient deviceLast appliedposition;delivery/read status12345

Follow the numbered arrows

  1. Sending device → Conversation serviceSend / retry m17
  2. Conversation service → History databaseCommit before persisted acknowledgement
  3. Conversation service → Sending devicePersisted s81 / sequence 81
  4. Recipient device → Conversation servicePoll after cursor / acknowledge
  5. Conversation service → Recipient deviceOrdered missing history
A persisted result is different from recipient delivery. Polling already works for offline catch-up and duplicate sends.
Trace the diagram in words
  1. The conversation transaction assigns position 81 and saves Mira’s m17 as message s81 before replying.
  2. If Mira retries m17, the service returns the saved s81/81 result rather than appending again.
  3. 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.
What this stage establishes
  • 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.

Read the reasoning for this stage →
Smallest working system
INTERVIEW STEP 6 / 7Go deep

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

  1. His phone connects to gateway G1; his laptop connects to G2. A gateway is the server holding that device's live connection.
  2. 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.
  3. 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.
  4. 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

  1. Devices send periodic “still connected” heartbeats. In this example, expire presence after 30 seconds without one.
  2. Ben can remain online while either approved device has recent activity. Typing indicators expire even sooner.
  3. 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

An accepted message survives duplicate delivery and a missing socketRepeated m17 maps to one accepted message. Each current device has a separate delivery route. An offline device recovers from history rather than gateway memory. Numbered arrows correspond to the connection key below. All connections are directional.m17 submittedtwiceOne intended sendrequestc8 historyOne message s81 / seq81Device sessionroutesBen: G1 phone, G2laptopOnline laptopStore one copy ofmessage s81Offline phoneResume after 75History catch-upFetch 76–8112345An accepted message survives duplicate delivery and a missing socketRepeated m17 maps to one accepted message. Each current device has a separate delivery route. An offline device recovers from history rather than gateway memory. Numbered arrows correspond to the connection key below. All connections are directional.m17 submittedtwiceOne intended sendrequestc8 historyOne message s81 / seq81Device sessionroutesBen: G1 phone, G2laptopOnline laptopStore one copy ofmessage s81Offline phoneResume after 75History catch-upFetch 76–8112345An accepted message survives duplicate delivery and a missing socketRepeated m17 maps to one accepted message. Each current device has a separate delivery route. An offline device recovers from history rather than gateway memory. Numbered arrows correspond to the connection key below. All connections are directional.m17 submittedtwiceOne intended sendrequestc8 historyOne message s81 / seq81Device sessionroutesBen: G1 phone, G2laptopOnline laptopStore one copy ofmessage s81Offline phoneResume after 75History catch-upFetch 76–8112345

Follow the numbered arrows

  1. m17 submitted twice → c8 historyFind saved send result before inserting
  2. c8 history → Device session routesSaved message-delivery work
  3. Device session routes → Online laptopMay deliver more than once
  4. Offline phone → History catch-upReconnect with cursor
  5. c8 history → History catch-upRead missing messages from history
Live routes speed delivery. Saved ordered history and message IDs let disconnected devices recover without displaying duplicates.
Trace the diagram in words
  1. Repeated m17 maps to one accepted message.
  2. Each current device has a separate delivery route.
  3. An offline device recovers from history rather than gateway memory.
See stage 2: Live transport gateways ↗

08Separate live gateways from durable ordering

  1. Separate connections from saving. Gateways hold live device connections; conversation services save messages. The directory tells delivery programs which gateway to contact.
  2. Partition by conversation. Give each conversation one ordering mechanism and replicate its accepted history under the durability promise before acknowledging saves.
  3. 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.
  4. 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.
  5. 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.
See stage 3: Independent recipient delivery ↗

09Follow an online send and an offline return

  1. The response is lost: Mira sends m17. The transaction saves s81 at position 81 plus the original result and pending delivery work, but Mira receives no acknowledgement.
  2. The send is retried: Mira resends m17. The application finds its saved result and returns s81/81. It does not append another message.
  3. A live device receives it: a delivery program finds Ben's laptop route. The laptop checks whether s81 is already stored; it saves one visible copy, then acknowledges delivered-through 81.
  4. 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.
  5. 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.
  6. 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.

INTERVIEW STEP 7 / 7Find bottlenecks and failure points

10State privacy, overload and ordering limits

  1. 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.
  2. 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.
  3. A writer is replaced: reject the old writer at the accepted write path as described above. A timeout alone is insufficient.
  4. 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.
  5. 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.
  6. 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

  1. Input: Ben selects messages he may currently read. Bound the range rather than sending his entire chat history.
  2. Output: a draft catch-up summary with links to the actual source messages. Saved history remains available to check exact wording.
  3. 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.
  4. 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.
  5. Fallback: return original permitted history without a summary when inference fails.
  6. 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.

  1. Show writer replacement and how retrying m17 returns the original s81.
  2. Draw how each device catches up without displaying 82 before missing earlier messages.
  3. Keep separate positions for saved, delivered and read progress.
  4. Calculate live delivery attempts for one message to 100 members with two connected devices each.
Your self-check
Compare the worked solution
  1. Let a replacement recover the committed message history. Prevent the old writer from making accepted writes before the replacement starts accepting new sends.
  2. Find Mira’s saved result for intended send m17: saved message s81 at conversation position 81. Return that result instead of adding another message.
  3. The delivery program looks up Ben’s current devices and retries s81. Each device checks its stored message IDs and keeps only one visible s81.
  4. His laptop can acknowledge 81 after applying it. His phone, last through 75, fetches 76–81 before applying an early live 82.
  5. Keep the highest delivered position and advance read-through only after Ben’s explicit read action. Connection presence does not prove either action.
  6. 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?
  1. Save one result for each sender’s intended send ID.
  2. Return that result on retry.
  3. Receivers store one visible copy per saved message ID.
  4. 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.
Practise this topic in the revision deck ↗

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.

1 / 10Mira retries m17 after the persisted response is lost. What should happen?
2 / 10The server writes s81 to Ben's socket. What can it claim immediately?
3 / 10Ben reconnects after 75 but receives a live 82. What maintains the conversation order?
4 / 10Ben’s phone has sent no heartbeat within the presence timeout, but his laptop session is fresh. Which presence interpretation fits?
5 / 10Why partition history by conversation?
6 / 10A slow device accumulates outbound bytes. Which response contains gateway resource use without losing accepted history?
7 / 10A new group member joins at sequence 100 under this lesson's policy. Which history may they read by default?
8 / 10Why avoid one physical queue topic for every conversation?
9 / 10A repeated delivered-through 81 arrives after delivered-through 84. What should storage do?
10 / 10Ben reads an AI summary of messages 76–81. May that automatically advance his read receipt through 81?
0 of 10 answered

CLOSE THE LOOP

Explain it without the page.

Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.

Go to the source