Prompt and applicable scenarios
A task editor immediately displays a submitted title. A user can submit title A and then title B before either request finishes. The second response may arrive first, either request may fail, a background refetch may complete while both are pending, and another user may update the same task. The server returns the canonical task and a monotonically increasing resource version after every accepted write.
Design the client state, request contract, ordering policy, failure recovery, conflict experience, accessibility feedback, and validation plan. The requirement is not merely to make the interface feel fast. After every completion order, the visible state must be explainable from an authoritative server state plus the user's still-pending intents.
This question fits senior frontend, UI infrastructure, and frontend system-design interviews. Public frontend interview material explicitly covers optimistic updates, request races, error states, and rollback, while a 2025 public interview record describes follow-up questions about optimistic-update principles and use cases. Those sources support the topic's present relevance; they do not establish a company-specific question or interview frequency. The category is frontend because the core task is browser-side async state and interaction design. Server concurrency control is an input to that design, not the primary implementation target.
What the interviewer evaluates
The first signal is whether the candidate separates authoritative state from speculative state. Replacing one object in a cache and saving an old snapshot for rollback works for one isolated request. It breaks when later optimistic work depends on the same object. A robust model keeps the latest confirmed base and an ordered collection of pending intents, then derives the rendered view from both.
The second signal is mutation semantics. “Keep only the latest response” protects one client render path, but it cannot stop an older request from being processed last by the server. The candidate must decide whether operations are serialized, coalesced, made commutative, or assigned a server-enforced order. The choice changes for “set title to B,” “increment by one,” “toggle,” “delete,” and a payment command.
The third signal is selective recovery. When operation A fails after operation B was submitted, restoring the snapshot from before A can erase B. A strong answer removes or marks the failed operation, advances the confirmed base only from a valid authoritative response, and replays the remaining intents. If a server version conflict makes replay unsafe, the UI surfaces the current server value and asks for a deliberate resolution.
The final signal is production discipline: pending and error states remain operable with keyboard and assistive technology; cancellation is not mistaken for server rollback; and tests force every response order, failure order, refetch race, retry, and conflict instead of validating only the happy path.
Questions to clarify before answering
- What does an operation mean? An absolute
setTitle("B")can supersede an earlier title draft, whileincrement(1)may need both operations to commit. Atoggle()command is ambiguous under retries; an explicit target value is safer. Operation semantics determine whether coalescing is valid. - May the user submit again while a save is pending? Disabling the control gives simple serialization but may violate the editing experience. If continued input is required, preserve the local draft separately and either queue/coalesce submissions or use a versioned parallel protocol.
- Which system decides write order? If the server offers only unconditional last-arrival-wins writes, the client must serialize order-dependent saves. If the API accepts a base version or client sequence and rejects stale work, controlled parallelism becomes possible.
- Is the optimistic action reversible and low risk? Likes, labels, and drafts often suit optimistic feedback. Payments, destructive actions, permission-sensitive changes, and actions with irreversible external effects may need confirmation or a pending state instead of pretending success.
- Can a background source update the same record? Refetches, subscriptions, another browser tab, and collaborators can change the base. The state model must identify a resource version and define whether pending intents can be safely replayed onto a newer base.
- What must the user perceive? Clarify whether an individual row needs pending, retry, and conflict indicators; whether focus may move; and which save or failure messages must be announced without producing a live-region message for every keystroke.
30-second answer framework
“I would keep the latest server-confirmed task as the base and represent each local submission as an identified intent. The UI renders the pending intents over that base. For title replacement, my default is one in-flight save per task and coalescing queued drafts to the latest title; a request ID alone cannot stop the server from applying an old request last. On success I adopt the returned canonical version, remove that intent, and replay any remaining intent. On failure I remove only the failed intent, not restore a whole stale snapshot. A version conflict pauses automatic replay and shows both values. I would expose per-item pending and error status, preserve focus, and test reordered responses, mixed success and failure, refetch races, retries, conflicts, and offline recovery.”
Step-by-step deep dive
1. Define one invariant before choosing a library
For each resource, keep a confirmed base and an ordered pending list. Each pending entry has a stable client operation ID, the intent and payload, its submission order, and its current status. The displayed state is a pure projection:
view = fold(base, pending in logical order, applyIntent)The invariant is: the rendered view equals the newest accepted server state plus every local intent that is still eligible to apply. This model makes a late refetch or response an input to reconciliation rather than an instruction to overwrite the screen.
React's optimistic reducer pattern supports the same separation: when the base value changes while an Action is pending, React can re-run the reducer against the new base. A cache library can manage mutation lifecycle callbacks, but it does not choose the product's operation semantics. The invariant remains useful whether the implementation uses React state, TanStack Query, another client cache, or a custom store.
Do not maintain three unrelated copies called serverTask, formTask, and optimisticTask with ad hoc synchronization effects. Keep the unsent form draft separate because typing is not yet a mutation. Once submitted, convert it to an intent with a stable identity.
2. Choose ordering from the operation, not from latency
There are three useful policies:
| Policy | Suitable case | Cost or risk |
|---|---|---|
| Serialize per resource | Order-dependent writes; API has no ordering guard | Later work waits, but server order is provable |
| Serialize and coalesce | Only the latest unsent value matters, such as repeated title submissions | Intermediate submitted values are intentionally dropped |
| Parallel with a server contract | Independent or commutative operations, or API enforces base version/client sequence | More throughput, but reconciliation and conflicts are explicit |
For this title editor, serialize by task ID and coalesce queued unsent title changes. If A is in flight and B is submitted, show B optimistically but keep only B as the next network write. After A settles, send B against the newest accepted version. TanStack Query documents that mutations otherwise run in parallel and provides mutation scopes for serial execution; the same policy can be implemented without that library.
Parallel requests are safe only with stronger semantics. An API might reject a stale base version, accept a monotonically increasing client sequence for one editing session, or expose an operation that is genuinely commutative. React's documentation also warns that custom async Transitions do not guarantee request order; a higher-level ordered Action or an explicit queue is still required. Tracking the newest request ID solely in the browser prevents an old response from overwriting the visible B, but the server may still store A last. Aborting A also does not prove that the server did not commit it.
3. Reconcile success without trusting arrival order
Every response should identify the operation and return the canonical resource plus its version. With serialization, handle the active operation, adopt the returned base, remove the operation, then derive the view by replaying the queued intent. The queued B remains visible while A completes, so the screen does not jump back to A.
With controlled parallelism, match a response to its operation ID and validate its resource version or acknowledgement. Do not assign response data directly merely because the promise resolved. A response older than the current confirmed version cannot replace the base. Remove only operations the response actually acknowledges. If the server returns a canonical transformation, such as trimming a title, use that result as the new base before replaying later local intents.
A background refetch follows the same rule. If it returns version 12 while the current base is version 11, version 12 may advance the base; pending intents are then re-applied if their semantics allow it. A refetch with version 10 is stale evidence and cannot move the base backward.
4. Recover by operation, not by snapshot
Suppose A and B are visible optimistically, then A fails. Restoring the object captured before A removes B as collateral damage. Instead, mark A failed or remove it, keep B, and recompute the projection. For an absolute title assignment, B can still be sent against the current base. For an order-dependent delta, B may need to wait, be recalculated, or be rejected because its original premise no longer holds.
Separate failures into actionable states:
- A validation or permission failure is final until the user changes the input or access; show the rejected value and server reason near the control.
- A transient network failure can expose retry. Reuse the same operation identity only if the server contract makes that retry safe; otherwise first reconcile whether the original write committed.
- A version conflict means the base changed elsewhere. Adopt or fetch the current server value, compare it with the local intent, and automatically replay only an operation with a proven merge rule. For a title conflict, present current and proposed values instead of silently selecting one.
- An unknown outcome means the request may have committed even though the response was lost. “Rollback locally and retry as new” can duplicate a non-idempotent action.
Some actions should not be optimistic. If a failure would be hard to undo, changes authorization, charges money, or creates a misleading legal or business state, show an immediate pending acknowledgement and confirm only after the server accepts it.
5. Make speculative state visible and accessible
Optimistic does not mean indistinguishable from confirmed. Mark the affected task as saving, retain the submitted value, and provide a local retry or conflict action. Do not disable unrelated tasks. If serialization is used, distinguish the active save from a newer queued value so instrumentation and error messages attach to the right intent.
Preserve keyboard focus when a save succeeds, fails, or the cache reconciles. A status region can announce “Saving task title,” “Task title saved,” or “Save failed” without moving focus. W3C's status role has polite live-region semantics; create the status container before the message change and avoid announcing each keystroke. Connect field-specific errors to the field and keep visual status understandable without relying on color alone.
6. Test the state machine and observe the policy
Test the selected ordering policy rather than pretending every policy permits the same trace. For the serialized path, assert that B is visible but is not dispatched until A settles; then cover A success or failure followed by B success or failure, response loss followed by reconciliation, and a server transformation. For any supported parallel path, use a controllable transport and server stub to force A-then-B and B-then-A processing and response orders. Assert the rendered projection, queued operations, confirmed version, and final server value after each event.
Then inject a newer and an older refetch while a mutation is pending, a collaborator conflict, offline-to-online recovery, component unmount and remount, repeated submit, and cancellation after the server commits. Verify keyboard focus, status announcements, retry labels, and that an error belongs to the correct task and operation.
Production telemetry should separate perceived latency from correctness: optimistic render latency, confirmation latency, failure and rollback rate, conflict rate, queue wait, coalesced operation count, retry count, and reconciliation mismatch. A fast interface that often corrects itself to a surprising value has failed the product contract.
High-quality sample answer
“I would start by asking whether every submitted title must be saved or only the user's latest intent matters. Here the latest title matters, the API returns resource versions, and I do not need parallel writes for one task. I would therefore allow continued editing but serialize network saves by task ID. If A is in flight and the user submits B, the screen shows B immediately and B becomes the coalesced queued intent.
The state for that task has a confirmed base, an active operation, and at most one queued title intent. Each operation has a client ID and the base version it will submit. The rendered title is the queued value, otherwise the active optimistic value, otherwise the confirmed title. When A succeeds, I adopt the canonical task and version from its response. I do not render A because B is still pending; I send B against the new version. When A fails, I remove only A and still offer to save B if the failure is transient. A validation or permission failure stays attached to the rejected intent.
If the server reports that another user advanced the version, I stop automatic submission, fetch or adopt the current title, and show the server and proposed values for resolution. I would not rely on ignoring an old response, because that cannot prevent an old request from writing last on the server. I also would not treat abort as a rollback.
Each task exposes its own saving, queued, failed, or conflicted status. Focus stays on the editor, and a pre-existing polite status region announces submission outcomes without announcing every character. Tests control promise resolution and server processing independently, so I can prove the final UI and server value for reordered success, mixed failures, stale refetches, conflicts, lost responses, retries, offline recovery, and unmounts.”
Common mistakes
- Saving one pre-mutation snapshot and restoring it on any error → a late failure erases newer optimistic work → remove the failed operation and recompute from the confirmed base plus remaining intents.
- Keeping only the latest response ID → the UI may look correct while the server commits an older request last → serialize order-dependent writes or require server-enforced version or sequence semantics.
- Using one global loading flag → unrelated controls freeze and a failure cannot be tied to its resource → track pending and error state by resource and operation ID.
- Treating every mutation as replayable → toggles, deltas, deletes, and irreversible commands have different failure semantics → define an explicit intent and merge rule before enabling optimistic behavior.
- Assuming cancellation undoes the request → the server may commit before it observes cancellation → reconcile the authoritative state and design retries for unknown outcomes.
- Letting any fetch overwrite the cache → a stale refetch or late response moves confirmed state backward → compare resource versions and replay valid pending intents over the newest base.
- Hiding all pending state to make the UI feel instant → users cannot explain a later correction or retry the affected action → show local saving, failure, and conflict states while preserving the optimistic value.
- Testing only success in submission order → the hardest races remain unexercised → control response order, server order, failures, refetches, retries, and remounts in the test matrix.
Follow-up questions and answers
What changes if the user can edit offline for several hours?
Pending operations must be durable, scoped to the authenticated account and resource, and replayed only after the client refreshes authorization and authoritative versions. Store intent semantics and stable operation IDs, not captured UI snapshots. On reconnect, fetch the base first, discard operations the user explicitly canceled, and replay only operations with a valid merge rule. Expired permissions, deleted resources, and schema changes require a visible blocked state. For long offline collaboration, a simple optimistic queue may be insufficient; the product may need domain-specific merge operations or a collaborative editing protocol.
Can every mutation run in parallel if the server returns versions?
No. A version tells the client which state a response represents, but it does not automatically define how two writes should be ordered or merged. Parallelism is safe when the server atomically checks the submitted base version, enforces a sequence, or exposes independent or commutative operations. Otherwise two absolute writes can still arrive and commit in the wrong order. Serialization is often the clearer contract for one order-dependent resource.
How would you handle optimistic creation when the server assigns the real ID?
Create a stable client ID before rendering and use it as the operation identity and temporary list key. On success, record the mapping to the server ID and replace the confirmed entity without remounting unrelated rows. Deduplicate a retry by the operation identity if the server supports it. If creation fails, remove or mark only that optimistic entity. Later operations targeting the temporary entity must wait for the mapping or be expressed in a queue that can rewrite the target after confirmation.
When would you deliberately choose pessimistic UI?
Choose it when showing success would materially mislead the user, recovery is not local, conflicts are common, or the action is irreversible or high risk. A payment, permission grant, legal submission, or destructive bulk action can acknowledge the click immediately while displaying a real pending state, then show success only after authoritative confirmation. The interface can still feel responsive without claiming an outcome that has not happened.