Design: Real-Time Chat Quiz
ADVANCED › System Design
While the chat screen is in the foreground, which transport best fits low-latency, bidirectional messaging?
- An FCM data message for every individual chat message
- A persistent WebSocket connection
- HTTP short polling once per second
- Server-Sent Events (SSE)
Answer: A persistent WebSocket connection
WebSocket provides a single full-duplex connection with minimal per-message overhead. SSE is server-to-client only, polling wastes radio and battery, and FCM is throttled/best-effort, not a real-time pipe.
The app is backgrounded and the OS has closed its socket. How should new messages reach the user?
- Hold the WebSocket open indefinitely with a permanent foreground service
- Poll the server every 15 minutes using a periodic WorkManager job
- Rely on the OS to automatically resume the socket after Doze ends
- Send a high-priority FCM message to wake the app or post a notification
Answer: Send a high-priority FCM message to wake the app or post a notification
Doze and App Standby suspend background sockets and processes, so high-priority FCM is the supported wake path. A permanent foreground service is abusive and polling adds latency and battery cost.
To guarantee a consistent message order across all participants, what should be authoritative?
- Each client device’s local wall-clock timestamp, used as-is
- Whatever order the messages reach the UI thread in practice
- A server-assigned sequence number or server timestamp
- Alphabetical sorting of the message UUIDs after parsing them
Answer: A server-assigned sequence number or server timestamp
Client clocks drift and can be set incorrectly, so they can't define a global order. The server assigns the canonical sequence/timestamp and clients reorder to match it.
An optimistically-shown message is retried after a network drop. How do you prevent duplicate messages server-side?
- Send a stable client-generated message id as an idempotency key
- Delete and recreate the message on every retry attempt instead
- Disable retries entirely so the message is only sent once there
- Append a random suffix to the body on each attempt to avoid clashes
Answer: Send a stable client-generated message id as an idempotency key
A stable clientMessageId lets the server recognize and dedupe replays of the same logical message. The other options either drop messages or actively create duplicates.
Which approach paginates chat history reliably as new messages keep arriving?
- OFFSET/LIMIT by page number, which shifts as new rows arrive
- Cursor/keyset pagination using a message id or time boundary
- Loading the whole thread into memory before paging through it
- Randomly sampling messages from the thread instead of paging
Answer: Cursor/keyset pagination using a message id or time boundary
Cursor/keyset pagination anchors to a stable boundary, so inserts don't shift pages. OFFSET drifts (skips/duplicates rows) and degrades for deep pages.
Following Android's recommended architecture, what should be the single source of truth for the displayed messages?
- The raw WebSocket stream, bound straight to the UI
- The ViewModel's in-memory list, kept as the source
- The local Room database, observed through Flow
- The server, re-queried on every recomposition cycle
Answer: The local Room database, observed through Flow
Offline-first design makes the local DB the SSOT; network and socket events are written into it and the UI observes it reactively, so it renders the same online or offline.
What is the most battery- and bandwidth-friendly way to handle typing indicators and read receipts?
- Debounce/throttle them, batch updates, and auto-expire typing
- Send a network event on every keystroke and every scroll update
- Open a new WebSocket connection for each indicator event sent
- Send each one as a separate synchronous HTTP request immediately
Answer: Debounce/throttle them, batch updates, and auto-expire typing
These are high-frequency, low-value signals, so throttling and batching minimizes radio wakeups and battery drain while still feeling live. The other options multiply network and connection overhead.
TCP can leave a WebSocket half-open (silently dead) after the radio drops. How do you detect that quickly at the app layer?
- Send app-level ping/pong heartbeats; close on a missed pong timeout
- Rely solely on OS TCP keepalive to spot every broken link immediately
- Reopen the socket on a fixed one-second timer, even when it is healthy
- Assume a dead socket always throws an exception on the very next read
Answer: Send app-level ping/pong heartbeats; close on a missed pong timeout
OS TCP keepalive is slow and unreliable on mobile, so an app-level ping/pong with a pong-timeout is the standard way to notice a half-open connection promptly and trigger reconnect.
When many clients lose connectivity at once (e.g. after a server blip), how should each app schedule its WebSocket reconnect attempts?
- Reconnect immediately in a tight continuous loop until it succeeds
- Use a fixed 30-minute retry interval for everyone
- Never reconnect and wait for the user to relaunch the app
- Use exponential backoff with randomized jitter
Answer: Use exponential backoff with randomized jitter
Exponential backoff caps reconnect frequency and the random jitter spreads attempts out in time, preventing a synchronized thundering-herd that would overwhelm the server on recovery.
You want WorkManager to flush the persisted offline send queue once the device regains connectivity. Which setup fits best?
- An always-on foreground service kept running around the clock for network checks
- A unique expedited one-time work request constrained to connected network
- An exact AlarmManager alarm set to trigger during Doze mode and wake it up
- A periodic worker without constraints that runs once every minute forever
Answer: A unique expedited one-time work request constrained to connected network
A constrained one-time worker runs only when the network is available and uniqueness avoids piling up duplicate flush jobs; a permanent foreground service is abusive and an unconstrained minute timer wastes battery.
The client receives messages tagged with server sequence numbers and sees the sequence jump from 100 straight to 103. What is the correct response?
- Render them in arrival order and ignore the missing range
- Wipe the local thread and reload the entire conversation from scratch
- Detect the 101–102 gap and request a targeted backfill for it
- Block the UI thread until the missing messages eventually arrive
Answer: Detect the 101–102 gap and request a targeted backfill for it
Monotonic server sequence numbers let the client spot gaps and fetch exactly the missing range, restoring correct order and completeness without an expensive full reload.
After a reconnect, the same message can arrive both over the socket and via an FCM-triggered fetch. How do you keep it from appearing twice in the UI?
- Upsert into Room by server message id so the second write no-ops
- Trust whichever copy arrives first and ignore the canonical server id
- Append both rows and let the user manually delete the duplicate later
- Disable FCM completely whenever the WebSocket is currently connected
Answer: Upsert into Room by server message id so the second write no-ops
An idempotent upsert keyed by the server message id collapses redundant deliveries into one row, so duplicate arrivals from different paths can't produce duplicate UI items.
How should large image or video attachments be transmitted in a chat message?
- Base64-encode the whole file and stream it inside the WebSocket frames
- Cram the raw binary bytes directly into the FCM data message payload
- Upload the file to object storage, then send a small message with its URL
- Store the raw bytes in the Room row and rely on DB replication to move them
Answer: Upload the file to object storage, then send a small message with its URL
Decoupling the binary upload (resumable HTTP to object storage) from the small text/control message keeps the real-time channel lean, supports retries/progress, and avoids FCM's ~4KB payload limit.
You want infinite-scroll history that is fetched from the network but always rendered from Room as the source of truth. Which Paging 3 component bridges the two?
- A PagingSource querying the REST endpoint directly with no local Room cache
- Manual LIMIT/OFFSET SQL queries fired on every single scroll event
- A LiveData.map transform over the entire in-memory message list
- A RemoteMediator filling Room from the network for PagingSource to read
Answer: A RemoteMediator filling Room from the network for PagingSource to read
RemoteMediator handles boundary-triggered network loads and writes results into Room, while Room's PagingSource feeds the UI, giving an offline-first paged list with the database as the single source of truth.