Design the Send Message feature like Slack (UI + client logic)
Optimistic insert with a tempId → POST → reconcile on response. Local outbox queues failed sends with retry/backoff and offline support. WebSocket fan-out for receiving. Composer state separate from message list. Markdown / mentions / file uploads as composer plugins. Idempotency key on the request so retries don't duplicate.
The send-message feature is the heart of any chat UI. The interesting parts are optimistic UX, error handling, idempotency, and realtime fan-out.
Data model
type Message = {
id: string; // server id (after success)
tempId?: string; // client id (pre-send)
channelId: string;
authorId: string;
body: string;
status: "pending" | "sent" | "failed";
createdAt: string;
};Send flow
- User hits Enter. Generate
tempId = uuid(). - Optimistic insert message in the list with
status: pending. - POST
/messageswith{ tempId, channelId, body, idempotencyKey: tempId }. - On 2xx: replace temp message with server response (real id + timestamps).
- On error: mark
status: failed, show retry button, push to outbox. - WebSocket broadcast notifies other clients in the channel.
Idempotency
Send the tempId as an idempotency key. If the request retried at the network layer (mobile flaky), the server returns the same message instead of duplicating.
Outbox
Failed messages go in an in-memory + persisted outbox:
type OutboxEntry = { tempId, channelId, body, attempts, nextRetryAt };Retry with exponential backoff (1s, 2s, 4s, 8s, max 60s). When the user comes back online (navigator.onLine, window.online event), flush the outbox.
Composer state
Keep composer state separate from the message list — composer re-renders shouldn't re-render the list. Per-channel draft persistence (localStorage or server) so switching channels doesn't lose typing.
Realtime
WebSocket subscription per channel. Server pushes:
- New messages (from anyone, including yourself — but skip if
tempIdmatches local). - Edits, deletes, reactions.
On reconnect, fetch messages since last known timestamp (resync).
Pagination + ordering
- Reverse chronological (newest at bottom).
- Cursor-based pagination (
before=<id>). - Insertion in correct position (binary search by createdAt or use a sorted Map).
Edge cases
- Sending while offline — keep in pending, retry on reconnect.
- Slow network, user types more — composer remains responsive; sent message slides into list.
- Server returns conflict — message edited by another client; merge or warn.
- Long messages — chunked upload for attachments; progress UI.
- Rich content — markdown/mentions handled in composer; rendered on display.
- Edit / delete — separate ops; reconcile via server id; show "(edited)" indicator.
A11y
- Live region (
aria-live="polite") announces new messages. - Composer is a labeled
<textarea>or contenteditable with proper roles. - Keyboard: Enter sends, Shift+Enter newline; Esc cancels reply.
Interview framing
"Optimistic insert with a tempId, POST with the tempId as idempotency key so retries don't duplicate. Reconcile by replacing temp with server response on success; on failure mark as 'failed' with a retry button and push to a persisted outbox with exponential backoff. WebSocket subscription for incoming messages and edits. Composer state is separate from the message list to keep re-renders cheap. Drafts persisted per channel. Resync on reconnect by fetching messages since last timestamp. Live regions for a11y."
Follow-up questions
- •How would you handle 'someone is typing'?
- •How do you order messages from multiple realtime sources?
- •How would you implement message edits?
Common mistakes
- •No idempotency key — duplicate sends on retry.
- •Composer state inside the list — every keystroke re-renders the list.
- •No outbox — messages lost on offline send.
- •No resync after reconnect.
Performance considerations
- •Virtualize the message list. Avoid re-rendering on every keystroke. Diff WebSocket events incrementally.
Edge cases
- •Out-of-order WebSocket events.
- •Clock drift between client and server.
- •Same user on multiple devices.
- •Very long messages with attachments.
Real-world examples
- •Slack, Discord, Linear's comments, Notion comments, WhatsApp Web.