Design a messaging client: transport, ordering, delivery states, offline sending, media, notifications and multi-device behaviour.
Predict first
A late older message, an echo of our own message before its acknowledgement, a duplicate delivery and an unacknowledged message are applied. What does the conversation show?
struct Message { let clientID: String; var seq: Int?; var text: String }
enum Event {
case localSend(id: String, text: String)
case ack(id: String, seq: Int)
case incoming(id: String, seq: Int, text: String)
}
struct Conversation {
private(set) var messages: [Message] = []
mutating func apply(_ event: Event) {
switch event {
case .localSend(let id, let text):
guard !messages.contains(where: { $0.clientID == id }) else { return }
messages.append(Message(clientID: id, seq: nil, text: text))
case .ack(let id, let seq):
if let i = messages.firstIndex(where: { $0.clientID == id }) { messages[i].seq = seq }
case .incoming(let id, let seq, let text):
if let i = messages.firstIndex(where: { $0.clientID == id }) { messages[i].seq = seq; return }
guard !messages.contains(where: { $0.seq == seq }) else { return }
messages.append(Message(clientID: id, seq: seq, text: text))
}
}
var ordered: [Message] { messages.sorted { ($0.seq ?? .max) < ($1.seq ?? .max) } }
}
var chat = Conversation()
chat.apply(.localSend(id: "c1", text: "on my way"))
chat.apply(.incoming(id: "c9", seq: 4, text: "where are you?"))
chat.apply(.incoming(id: "c1", seq: 5, text: "on my way"))
chat.apply(.ack(id: "c1", seq: 5))
chat.apply(.incoming(id: "c9", seq: 4, text: "where are you?"))
chat.apply(.incoming(id: "c8", seq: 3, text: "hello"))
chat.apply(.localSend(id: "c2", text: "see you soon"))
for m in chat.ordered { print(m.seq.map(String.init) ?? "-", m.text) }
- 3 hello, 4 where are you?, 5 on my way, then the pending message last
- The duplicate appears twice
- Order follows arrival order
Mental model. A messenger is a postal system where every letter gets a stamped serial number at the sorting office. Letters can arrive twice or late, but sorting by serial number and discarding repeats always gives the same bundle.
Requirements and invariants. One-to-one and group chat, text and media, works offline, shows delivery and read state, notifies when backgrounded, multiple devices per account. Invariants: a message is never lost or duplicated, order is the same for every participant, and the UI is instant. Source of truth: a local database that the UI observes; the network only feeds it. Transport: a persistent socket while in the foreground for low latency (URLSessionWebSocketTask with your own reconnect, heartbeat and backoff, Q321), and push for the background (APNs wakes the app or a notification service extension, with limits, Q375); fall back to polling or fetch-on-open when the socket is unavailable. Ordering and identity: the server assigns a monotonically increasing sequence number per conversation and that is the order everyone sees; the client generates a client message ID (a UUID) for every message it sends. Delivery is therefore at least once with an idempotent consumer: the server and client both discard a message they have already seen by its id, which gives effectively-once behaviour. The check below applies events out of order and with duplicates (a late old message, an echo of our own message arriving before the acknowledgement, a duplicate delivery) and still ends with one copy of each in sequence order, with the unacknowledged message pending at the end.
Sending. Insert the message locally immediately (optimistic) with state sending; a send queue persisted in the database retries after a crash or while offline with backoff (Q324, safe because of the client id); the acknowledgement attaches the server sequence and moves the state to sent, then delivery and read receipts move it to delivered and read; a permanent failure shows failed with retry and delete. History loads by sequence cursor, newest first, with gap detection (if you see sequence 8 then 11, fetch 9 and 10) rather than relying on timestamps (Q318). Media: create the message with a placeholder, upload with a resumable background session (Q322), send the reference when done, show thumbnails from a downsampled cache (Q327). Multi-device and sync: each device keeps its own cursor and read state, reconciled by the server (Q340). Privacy and security: if messages are end-to-end encrypted, the server cannot search or generate previews, so search is local, push content comes through an extension that decrypts on device, and keys are per device. Operations: reconnect storms after an outage need jittered backoff (see the reliability question), typing and presence are ephemeral and never persisted, and metrics are send success rate, time to delivered and reconnect rate.
Sequence numbers fix the order, ids make duplicates harmless, and an unacknowledged message stays last until the server numbers it.
Common traps
- Ordering by device timestamps. Clocks differ and drift; use a server sequence per conversation.
- Retrying a send without a client message id. The retry can create a duplicate; the id makes it idempotent.
They might follow up with
- How do you detect a missing message? A gap in the sequence numbers, then fetch the missing range.
- What does the UI observe? The local database, so sending and receiving both update it and the UI never talks to the network directly.
Related questions
- 26.1.1 How do you approach a mobile system design question: what do you clarify, in what order do you design, and…
- 26.1.3 Design an offline-first reader app: content model, sync, storage budget, prefetching, reading position and…
- 26.2.1 How do you design a client for reliability: deadlines, circuit breakers, degradation tiers, bulkheads, kill…
- 12.4.1 How does URLSessionWebSocketTask work, and what do you have to handle yourself?
- 12.5.1 How should a client retry failed requests: what to retry, how long to wait, and what idempotency has to do…
- 13.7.1 How would you design multi-device sync: identity, change tracking, conflict resolution, deletions and…
- 17.1.2 How does the Apple Push Notification service deliver a push: tokens, priority, collapse, expiry, and what can…
← How do you approach a mobile system design question: what do you… Design an offline-first reader app: content model, sync, storage… →