Day 46: Design: Real-time Chat Application (Slack/WhatsApp Web)
Practice a full frontend system design answer for a chat app: connection strategy, message ordering/delivery guarantees, and optimistic UI.
Study
Concepts
Framing the requirements before designing anything
Start by stating functional requirements out loud (send/receive messages in real time, delivery/read receipts, offline message queueing, typing indicators) and non-functional ones (low latency, works on flaky mobile networks, must not lose messages, must handle reconnection gracefully) — a senior answer always separates these before touching architecture, because they directly drive the WebSocket-vs-polling and local-storage decisions below.
The core building blocks: a WebSocket connection (Day 43) for real-time push, a local message store (Day 45's offline-first ideas) as the source of truth for the UI, and an outbox for messages sent while disconnected — a chat app is essentially a focused case study combining networking, offline-first, and optimistic UI into one product.
Message ordering, delivery states, and optimistic sending
Each message needs a client-generated ID (sent immediately, rendered optimistically as "sending") and a server-assigned sequence number/timestamp used for the CANONICAL order once acknowledged — reconciling "my optimistic local order" with "the server's authoritative order" when messages from multiple senders interleave is the central hard problem, usually solved by re-sorting by server timestamp once acks arrive, while keeping the optimistic message pinned in place until either confirmed or failed.
Delivery states (sending → sent → delivered → read) are modeled as a small state machine per message, driven by WebSocket events (`ack`, `delivered`, `read_receipt`) — exactly the kind of explicit state machine covered in Day 49, applied to a different domain.
See It
Visualizations
Visualization
Chat app client architecture
Visualization
Per-message delivery state machine
optimistic, client-generated ID, shown instantly
server acked, assigned canonical sequence
recipient device received it
recipient opened the conversation
Build It
Code Examples
Optimistic send with reconciliation on server ack
function sendMessage(conversationId, text) {
const clientId = crypto.randomUUID();
const optimisticMessage = {
clientId, text, status: 'sending', createdAt: Date.now(), serverSeq: null,
};
// 1. Render instantly, before any network round trip
addMessageToLocalStore(conversationId, optimisticMessage);
// 2. Queue for the socket (works even if currently offline — flushed on reconnect)
enqueueOutbound({ clientId, conversationId, text });
}
// 3. When the server acknowledges, reconcile by clientId — never re-render
// a "new" message, just update the existing optimistic one in place.
socket.on('message:ack', ({ clientId, serverSeq, serverTimestamp }) => {
updateMessageInLocalStore(clientId, {
status: 'sent',
serverSeq,
createdAt: serverTimestamp, // now sortable against everyone else's messages
});
});Remember
Key Takeaways
- State functional + non-functional requirements FIRST — they drive every subsequent architecture decision, and interviewers grade this explicitly.
- Combine a WebSocket (real-time push) with a local-first store (offline resilience) and an outbox (reliable send) — this is the same trio from Days 43/45, applied together.
- Client-generated IDs enable optimistic rendering; server sequence numbers/timestamps provide the canonical order once acknowledged.
- Model delivery status (sending/sent/delivered/read) as an explicit per-message state machine driven by socket events.
- Always mention reconnection/backoff and message-history pagination — both are easy to forget but expected in a complete answer.
Do It
Practice
- 1Whiteboard (or write out) the full architecture diagram for this system from memory, including the outbox and reconciliation step.
- 2Implement the optimistic-send-and-reconcile-by-clientId pattern against a mock WebSocket server.
- 3Extend the design to support "typing…" indicators — decide what transport they use and why they should NOT go through the reliable outbox.