Two writes to the same item are in flight and the row flickers back to an old value; how do you make the settled state deterministic?
answer
- arrival order is not application order
- the last response is not the newest intent
- derive from base plus pending patches
- remove a patch by identity, never restore a snapshot
- one scoped revalidation when the queue drains
basics
~20 sStop treating each response as the final word. Track how many writes to that entity are in flight, derive the view from a base value plus ordered pending patches, ignore or queue superseded writes, and revalidate once nothing is pending.
solid answer
~50 sThe flicker has two usual causes, and both come from treating responses as authoritative in arrival order. Either the earlier write's response lands last and overwrites the later change, or the earlier write fails and its rollback restores a value saved before the later patch existed. The fix is to make the settled value a function of state you control: keep a per-entity count or list of writes in flight, render the base value plus the pending patches in order, remove a patch by identity rather than restoring a snapshot, and only accept a server representation as the base when no write to that entity is pending. If the writes genuinely must not interleave, serialise them per entity in a queue; when a later write fully replaces an earlier one, collapse the queue instead of sending both. Then revalidate once, on drain.
go deeper
Take away the core distinction: the response that arrives last is not necessarily the change the user made last. Older data overwriting newer data is the bug to recognise.
Explain the interleaving step by step, and why a rollback that restores a saved value can undo a change that came after it.
Show the mechanism you would build: pending writes tracked per entity, a derived value rather than in-place mutation, patches removed by identity, one scoped revalidation when nothing is pending.
Decide where this lives. Per-screen concurrency handling is how the same bug ships five times; the ordering rules belong in the shared data layer, with collapsing policy stated per write kind.
## The sequence that produces the flicker Spell it out, because the fix follows from the ordering: 1. The user changes a field. Write A is patched locally and sent. 2. Before A answers, the user changes it again. Write B is patched locally and sent. 3. B answers first. Its representation is written into the cache as the base value. The screen is right. 4. A answers. Its representation describes the world **before** B, and is written in as the base value. The row flickers back. There is a second, nastier variant. If A fails and its rollback restores the value saved at step 1, it reinstates a pre-B value — and this time there is no later response coming to correct it. Note what is *not* the problem: the server may have applied A then B in the right order. Application order and response arrival order are different things, and the client sees only the second. ## Why the obvious fixes do not work - **"Last response wins."** That is exactly what step 4 does. The last response is not the newest intent, only the slowest request. - **"Disable the control while pending."** It reduces the frequency and does not remove the case: the second change can come from another control, a keyboard shortcut, a list-wide action, or another tab. - **"Refetch after each write."** A refetch is one more read racing the writes. It can observe the state between A and B, and it can land after B's own response — the same flicker, now with extra requests. - **"The server serialises it."** The server's order is not visible to the client; the client's screen is ordered by arrival. ## Three strategies that do work | strategy | mechanism | good for | cost | | --- | --- | --- | --- | | serialise per entity | queue writes for one entity; send the next only when the previous settles | additive or path-dependent changes | slower for a rapid run of edits | | supersede | a later write replaces a pending earlier one; drop the earlier or ignore its response | whole-value replacements, toggles, renames | wrong for increments and appends | | base plus ordered patches | view value = last confirmed base plus pending patches in order; base accepted only when nothing is pending | almost everything | the bookkeeping has to be written once | The third is the general answer and worth being able to describe precisely. Each pending write owns a patch with an identity. The rendered value is derived, never mutated in place. A response removes its own patch and, if it is the newest settled write and no other is pending, updates the base. A failure removes its patch and nothing else. Because nothing ever restores a saved snapshot, a rollback cannot undo somebody else's change. ## Settling: one revalidation on drain When the queue drains, do a single scoped revalidation of that entity rather than one per write. It costs one request for a run of edits, it cannot race the writes because none is outstanding, and it is the moment at which the client can finally trust what it reads. Coalescing here also fixes the second-order problem of a rapid editor generating a request storm. ## Collapsing what can be collapsed Before reaching for a queue, ask whether both writes need to exist: - a toggle switched twice may be a no-op — send nothing; - a rename typed in three bursts is one write, not three; - a counter incremented twice is **not** collapsible, and neither is a pair of appends — folding those loses a change. Getting this distinction right is usually worth more than any ordering machinery, because it removes the concurrency rather than managing it. ## Retry belongs to this conversation too A retried write is another write in flight, so it goes in the same queue and obeys the same rules — a retry that bypasses the ordering logic reintroduces the flicker in its worst form, since a retried older change can land long after the newest one. Whether a failed write may be resent at all is not the client's judgment to make alone; it depends on what the API promises about repeated application, and that promise is part of the endpoint's contract rather than something the component can infer from a failure. ## What to demonstrate Interviewers are listening for two things: that you distinguish application order from arrival order, and that your rollback is scoped by patch identity rather than by restoring a saved copy. A candidate who says "I keep the pending writes and derive the value" has already answered the follow-ups.
- Why does refetching after every write not fix the flicker?Because a refetch is another read racing the outstanding writes. It can observe the entity between the two changes, and its response can land after the newer write's response — reproducing the same flicker with more requests. Ordering has to be resolved in client state; a refetch is only trustworthy once no write to that entity is pending.
- When is dropping a pending earlier write the right move rather than queuing it?When the later write fully supersedes it: a toggle, a rename, any whole-value replacement. Collapse them and send one request. It is wrong for additive changes — increments, appends, list insertions — where each write carries a distinct effect and folding them loses one of the user's actions.
- How would you test this deterministically?Drive the component through a fake transport whose responses you release by hand, then assert the settled value for each interleaving: B before A, A failing after B succeeded, both failing, a retry landing last. The bug only ever appears in a specific arrival order, so the order has to be an input to the test rather than a matter of timing.
saying these in an interview costs you the question
- Whichever response arrives last holds the correct value.
- Two writes to one entity cannot overlap if the control is disabled.
- Restoring the pre-write value is always a safe rollback.
- Refetching after each write resolves the ordering problem.
- The server serialises the writes, so the client need not care.
- A retry is separate from the pending writes and can be sent freely.