How does a version value survive the round trip when a record is read, edited in a client, and posted back?
answer
- the human gap, not the millisecond gap
- the client is a courier for the token
- a fresh load compares against itself
- the submitted version must reach the where clause
basics
~20 sThe version travels out in the transfer model and comes back with the submitted edit, so the write compares what the user actually saw. A path that re-reads the row and copies fields onto it compares nothing.
solid answer
~50 sThe gap the guard exists for is the human one - a record is read, displayed, edited for minutes, then submitted. To cover it, the **version must be part of the transfer model** in both directions: sent with the record, held opaquely by the client, and returned with the edit. On the write side the version that ends up in the update's `WHERE` clause must be the **client's** value, not one just re-read from the database. The usual failure is a write path that loads the row fresh, copies the submitted fields onto it and saves: the loaded version always matches itself, the guard passes every time, and the earlier editor's changes disappear. Either compare the submitted version against the loaded one and reject before writing, or apply the submitted version to the object so the guarded statement uses it.
go deeper
The version must be sent to the client with the record and sent back with the edit. If it does not make the trip, nothing checks whether the record moved while the form was open.
Explain why loading the row again on submit and copying fields onto it makes the check pass every time, and where the submitted version has to end up instead.
Walk the whole path - transfer model out, opaque echo back, comparison against the submitted value, rejection with the current state shown - and name the serialisation traps that quietly break the token.
Decide the contract: whether the version is mandatory on every write, whether some edits are re-applied as deltas instead of bounced, and what the user experience of a rejection is across all clients.
## The window this is really about A guard that compares versions inside a single short transaction rarely fires - the read and the write are milliseconds apart. The window that matters is the **detached** one: a record is read, rendered for a person, edited while they think, and posted back minutes later. Two people can easily open the same record in that time. If the version does not survive the trip out and back, the guard is decorative. ## What the transfer model must carry A transfer model is the flat shape the boundary hands to the client, not the tracked object itself. To keep the guard alive it needs one extra field: - **Outbound**, the version the record had when it was read, alongside the editable fields. - **Inbound**, the same value, returned unchanged with the submitted edit. - **Opaque to the client.** The client is a courier, not an author: it must never compute, increment or invent the value, only echo it. - **Mandatory on write.** A submission without a version cannot be checked, so the boundary should reject it rather than write blind. ## The mistake that silently disables the guard The write path most people reach for first is: 1. Load the row by identifier, in a fresh unit of work. 2. Copy the submitted fields onto the loaded object. 3. Let the unit commit. Every step looks reasonable, and the guarded update is genuinely emitted - with the version that was loaded **one millisecond ago**, which of course still matches. The check passes unconditionally. The edit that another user committed during the person's thinking time is overwritten with fields calculated from stale values, and nothing anywhere reports a problem. This is the single most common way an application ships version columns and lost updates at the same time. ## Two ways to make the submitted version the one that counts | Approach | How it works | Notes | |---|---|---| | Compare before writing | Load the row, compare its current version with the submitted one, and refuse if they differ | Simple and explicit; the tiny gap to the write is still covered by the guarded statement | | Write with the submitted version | Put the client's version onto the object so the guard compares it | Fewest moving parts, but only some layers let the field be set that way | Both end with the same property: a submission based on a stale view cannot be written. The explicit comparison also gives a natural place to build the message the user will see. ## What the user gets back A rejection is a product decision, not just an error code. The useful responses are: - **Show what changed.** Return the current values so the person can see who moved what, and re-submit against the new version. - **Re-apply the intent.** Where the edit is expressible as a delta rather than an absolute value - add a note, increment a count - the server can redo it against the current row instead of bouncing it back. - **Never silently accept.** Discarding the version and writing anyway is the behaviour the whole mechanism exists to prevent. ## Partial submissions and multi-step forms Two details bite in practice. First, if the client posts only the fields it changed, the version still has to come with them; a partial submission is not an excuse to skip the check. Second, a wizard that reads a record on step one and writes on step four should carry the version from step one, not re-read it at step three - otherwise the check only covers the last few seconds of a long interaction. ## Serialisation is part of the contract The value has to survive the encoding both ways. A counter passed through a format that treats large numbers loosely, or a timestamp truncated to a coarser precision when it is formatted, comes back as something that no longer equals what is stored - which turns into either a permanent false conflict or, worse, a comparison that matches when it should not. Keep the value in a form the wire format represents exactly, and treat it as a string token at the boundary if there is any doubt. ## The rule to remember The guard compares the state of the row against the state **the editor saw**. Every design decision above is in service of that single sentence: whatever is compared in the `WHERE` clause must trace back to the moment the human looked at the screen, not to the moment the server started handling the submission.
- The write path loads the row and copies the submitted fields onto it. Why is the guard useless there?Because the version bound into the statement is the one just read, and it necessarily still matches. The comparison is against the server's own read a moment ago, not against what the user saw, so any edit committed during the thinking time is overwritten without a word.
- Should the client be allowed to compute or omit the version?No. It is an opaque token to be echoed back exactly as received. A client that invents one can pass a check it should fail, and a submission with no version cannot be checked at all - the boundary should reject that rather than write unguarded.
- What if the client legitimately submits only the fields it changed?Partial submissions are fine, but the version still travels with them. Field-level submission narrows what is written; it does not narrow what must be verified, because the invariant being protected may span fields the client left out.
saying these in an interview costs you the question
- Re-reads the row on write and calls the guard satisfied
- Leaves the version out of the transfer model entirely
- Lets the client compute or bump the version
- Treats a missing version on submit as permission to write
- Thinks a short server-side transaction closes the editing window
- Overwrites when the check fails rather than showing the current values