We design a comment system that gives users immediate visual feedback (optimistic posting) while ensuring consistency and durability. The system must handle temporary identities, idempotent creation, response/event reconciliation, failure recovery, and live synchronization across multiple clients.
POST /comments), fetching history (GET /comments?page=...), and optional update/delete.id (server-generated, UUID or snowflake), thread_id, author_id, content, created_at, idempotency_key (client‑supplied temporary ID), and status (active, hidden, deleted).POST, the client creates a UUIDv4 and prefixes it with temp- (e.g., temp-4a7d1ed5-...). This is the idempotency key.201 Created with the real ID, created_at, and any moderating status.200 OK with the existing comment data (real ID, timestamp, etc.). This handles retries safely, even if the original response was lost.created_at or a monotonic id determines the canonical order once acknowledged.{
"id": "temp-...",
"content": "...",
"author": "...",
"createdAt": "now (client‑side)",
"status": "pending"
}
The UI renders it immediately, maybe with a faint “Sending…” indicator.POST /comments with body:
{
"thread_id": "...",
"content": "...",
"idempotency_key": "temp-..."
}
When the POST promise resolves (or times out), the client reconciles the optimistic comment:
201 success:
idempotency_key.createdAt to the server timestamp (may cause the comment to shift position in a chronologically sorted list).status to "posted" (or "pending_moderation" if content goes through a pipeline).409 Conflict or duplicate: The server returns the already‑created comment (because idempotency key consumed). Treat identically to a 201; the client may not have known the comment succeeded earlier (e.g., due to a lost response). Reconcile the same way.400 Bad Request (e.g., validation failure, spam):
status = "error" with the server’s error message.status = "pending". Display an “Uploading…” indicator with an option to manually retry."new_comment" event to all subscribers of the thread, including the original poster. The event payload contains the real ID, content, author, timestamp, and the idempotency_key (if supplied) for client deduplication.new_comment event arrives:
event.idempotency_key matches any pending comment’s temp ID.new_comment event later arrives (since the server broadcasts to all, including the poster), the client must recognise the real ID already exists in its local list – skip it.localStorage and re‑apply them after a page reload, but this is optional.GET /comments?thread_id=...&before=<cursor> paginated by created_at and optionally monitor‑friendly. All comments in the history are in canonical order.created_at (server timestamp).Can comments be edited or deleted?
Yes. Editing and deletion can also be optimistic:
PATCH /comments/:id with new content and an updated timestamp. Optimistically update the local comment; on failure, revert. Idempotency can be achieved by including a version or last_modified header, but simpler: use a new idempotency key for each edit if needed.DELETE /comments/:id; optimistically remove the comment from the list (or grey it out). On failure, restore.How are comments ordered?
Primarily by server‑generated created_at (with sub‑millisecond precision or tie‑breaking by a monotonic ID). Client‑side pending comments are inserted near the top or bottom with a placeholder timestamp; they are reordered when the real timestamp arrives.
Timeout with unknown commit outcome?
Retry with the same idempotency key. Because the server deduplicates, this is safe. The client continues to show the comment as pending until a definitive response (success or server rejection) arrives.
Must another device see the same conversation?
Yes, but only after the server has accepted and persisted the comments. Optimistic drafts are local and invisible to others; this is by design.
What if a live event arrives before the POST response?
As described in §5, the client uses the idempotency key from the WebSocket event to reconcile the pending comment immediately, ignoring the (now redundant) HTTP response.
How do you avoid duplicate comments on retry?
Idempotency keys on the server guarantee that the same logical comment is never created twice. The server must atomically check and insert using a unique constraint on (thread_id, idempotency_key).
What if the server rejects content (spam/validation)?
The client enters an error state, allowing the user to edit and resubmit. When resubmitting after a hard rejection, a new temporary ID must be generated so that the corrected content is treated as a brand‑new attempt, not a duplicate of the rejected one.
What if the user navigates away?
Pending optimistic comments are lost from the current page. The comment may still be accepted by the server (if the POST was already in flight). When the user returns, the comment will appear in the history if the POST succeeded. To improve the experience, the client could show a confirmation dialog if there are unsent changes, and/or persist pending drafts to localStorage and re‑apply them on page load with a flag indicating they need to be synced (re‑POST if the server did not receive them). However, this adds complexity; a minimal robust design simply discards unsent drafts and relies on the server’s eventual consistency for in‑flight posts.
This design balances immediate user feedback with system integrity, leverages idempotency to handle network unreliability, and reconciles optimistic state with both HTTP responses and real‑time events to avoid inconsistencies.