Two callers read one entry holding a quota count, each adds ten, and both write back — what is stored, and what is reported?
answer
- three steps, not one
- the gap sits on the caller's side
- per-operation atomicity does not compose
- nothing is reported to anyone
- last-writer-wins by default
basics
~20 sThe second write lands whole and the first caller's addition is gone. An entry that held 100 holds 110, not 120 — and nothing is reported: both callers were told their write succeeded. That default outcome is last-writer-wins.
solid answer
~40 sEach caller does three things: read the entry, compute the new value in its own code, write the value back. The store applies each of those operations whole, but the read and the write are two operations, and nothing holds the entry across the gap between them. Both callers read 100, both compute 110, both write 110 — the second write covers the first, and one addition of ten is gone. The part candidates miss is the silence: no error, no conflict, no version bumped, nothing in a log. Both writes genuinely succeeded; the store was simply never told that the value being written was derived from the value read. This is `last-writer-wins`, and it is the default on any store where you do read-modify-write without a remedy.
go deeper
Recall the shape: read, compute locally, write back is three steps, and two callers can interleave them. Be able to state the final stored value for a given interleaving and to say that neither caller is told anything went wrong.
Explain the boundary rather than the outcome: a single operation against one entry is applied whole, but a read and a write are two operations with an unprotected gap between them, so per-operation atomicity does not compose into a safe update.
Volunteer the silence and its operational consequence: this never appears in the store's error metrics, so it is found as drift against an independent record. Then name which remedy you would reach for and why, given what the server can interpret.
Frame it as a contract question. This tier offers no isolation level and no rollback, so every invariant kept here is kept by a convention the callers agree to. Decide which invariants are worth enforcing on the store and which belong somewhere that can refuse a write.
## What the two callers actually do A caller that changes a stored value it did not compute locally always performs three steps, not one: 1. **Read** the entry and receive the current value. 2. **Compute** the new value in its own process — add ten, append an item, set a field of a document. 3. **Write** the new value back over the old one. The store sees steps 1 and 3. Step 2 happens on the caller's machine, one network hop away and some milliseconds later. Nothing in the store is holding the entry during that gap, and nothing in the store has been told that these two operations belong together. ## The interleaving | time | caller A | caller B | stored value | |---|---|---|---| | t1 | reads 100 | | 100 | | t2 | | reads 100 | 100 | | t3 | computes 110 | | 100 | | t4 | | computes 110 | 100 | | t5 | writes 110 | | **110** | | t6 | | writes 110 | **110** | Both callers added ten to a value of 100. The entry holds 110. Ten units of somebody's work are gone, and the arithmetic was correct on both machines. Nothing here is a bug in the store, in the network, or in either caller taken on its own. ## Why per-operation atomicity does not save you Stores of this class promise that a single operation against a single entry is applied whole: no other caller ever observes it half-done. That promise is real, and it is not the promise you need. **Atomicity of each of two operations says nothing about the pair.** "Atomic" means no one sees your operation half-applied — not that no one runs between your operations. The two execution models in this class both deliver that same per-operation promise, by different means: - Some stores use **one-operation-at-a-time execution**: the server starts an operation and finishes it before starting the next, across the whole keyspace. - Others run **many worker threads** and reach the same guarantee by **per-entry locking** — the entry, or a stripe of entries, is locked for the duration of one operation. - The models differ in their consequences elsewhere: head-of-line blocking behind one long operation is characteristic of the first, contention on one entry's lock of the second. - **Neither makes a read-modify-write safe.** The gap is on the caller's side of the wire, where no server-side mechanism reaches. A candidate who answers "it runs one operation at a time, so it is atomic" has stopped at the true half of the model and missed the boundary. ## The silence is the defining property When a database detects a conflict, something in the system knows. Here, nothing does: - Both callers received a success acknowledgement, and both were right to. Each write did exactly what it was asked to do. - No version was bumped, because nothing versioned the entry. - No error appears in the store's error metrics or its logs, so the usual monitoring will never surface it. - The loss shows up much later, and somewhere else: a count that drifts below an independent tally, a field a user swears they saved, an allowance that is spent twice. It helps to keep the three things a caller can actually observe apart, because in a diagram they look alike: | what happened | what the caller sees | |---|---| | an operation group was abandoned before any step applied | an explicit "not applied" outcome | | a group applied and one step errored while applying | partial effects, and an error the caller must repair from | | last-writer-wins on an unprotected read-modify-write | **nothing at all** | ## There is no transaction to open here The remedies a relational engine offers for this same race are not on this menu: a store of this class gives you no isolation level to choose and no rollback to fall back on. What it gives you instead is three ways to remove the gap: - **Move the change onto the server.** Where the server can interpret the value — a counter, a field of a map, a member of a set — ask it to make the change in place, so the read-modify-write never crosses the network. Where the server only stores and returns bytes, this remedy does not exist. - **Declare what you read.** Present a version token read alongside the value, or name the entries you read before submitting a group, so that a write based on a stale read is refused or the group is abandoned instead of landing. - **Hold a claim.** Create a marker entry only if none is there, with a deadline and an owner marker, and run the cycle only while you hold it. Which one fits depends on whether the server can read the value, how contended the entry is, and how long the protected work runs. That choice is the real interview question; the interleaving above is only the setup. ## What an interviewer is listening for - The stored value stated precisely (110, not "it depends"), and the reason stated as two operations with a gap. - The silence volunteered unprompted, rather than an assumption that something must have errored. - An awareness that the execution model does not change the answer. ## Both callers are acknowledged. The expected value was 120; the entry holds 110, and no participant is told that anything was lost ``` time caller A caller B stored value t1 read entry -> 100 100 t2 read entry -> 100 100 t3 compute 100 + 10 = 110 100 t4 compute 100 + 10 = 110 100 t5 write entry = 110 110 t6 write entry = 110 110 ```
- Does the answer change if the server runs many worker threads instead of one operation at a time?No. Both designs give the same per-operation atomicity — one by finishing an operation before starting the next, the other by locking the entry for the duration of the operation — and both leave the caller-side gap between the read and the write untouched. What differs is the consequence elsewhere: head-of-line blocking behind one long operation on the first, lock contention on a hot entry on the second.
- Two callers rewrite different fields of the same stored document. Is anything still lost?Yes, wherever the caller writes the whole value back: the later write carries the earlier writer's field at its stale value, so that edit disappears even though the two callers never touched the same field. Where the server can interpret the value and change one field in place, the two edits touch different parts and both survive — which is exactly why whether the server understands the value decides the remedy.
- If both writes succeeded, in what sense is this a failure at all?The failure is at the level of the caller's intent, not the operation. Each caller intended "add ten to whatever is there", and the store executed "store 110". Both executions were correct; the derivation from the value read was never expressed to the store, so it could not be protected. Naming it that way is what points at the remedy.
Two people photograph a whiteboard list, each adds one item at their own desk, then each rewrites the whiteboard from their photo. The second rewrite erases the first person's item, and the whiteboard raises no objection.
saying these in an interview costs you the question
- Says the server runs one operation at a time, so the race cannot happen
- Expects the losing write to fail with a conflict error
- Answers 120, as if the store applied both additions
- Reaches for an isolation level this class of store does not offer
- Calls a group of operations a transaction and expects a rollback
- Assumes a retry fixes it, when nothing failed to retry from