beginner 3 min answer Multiple choice

Two support agents open the same customer record in an internal console. Both edit a different field and save within forty seconds of each other. The second save silently discards the first agent's change, and nothing in the logs looks like an error. Which mechanism prevents this?

httpetagoptimistic-concurrencylost-updatebeginner
Pick one
Show the full answer Hide the answer

What is being tested

Whether you recognise the lost update as a protocol problem rather than a UI problem. Read-modify-write over a stateless API has no built-in notion of "the version I was looking at", so the last writer wins by default and the loss is silent. There is no error to find, which is why this reaches production and stays there.

The mechanism

A full-document PUT carries the whole resource, including the fields the agent did not touch. Agent B's payload was built from a snapshot taken before agent A saved, so B's request faithfully asserts the old value of A's field. The server applies exactly what it was asked to apply.

HTTP's conditional-request machinery (RFC 9110, 2022, section 13) closes this. The GET returns ETag: "v41", an opaque validator for that representation. The PUT sends If-Match: "v41". The server compares the validator before applying anything and returns 412 Precondition Failed if the resource has moved on, which turns a silent overwrite into a visible conflict the client can resolve by re-reading and retrying.

Two implementation details decide whether this works in practice:

  1. Require the header, do not merely honour it. A server that applies an unconditional PUT has an optional safety feature, which means it has none. Reject the unconditional write with 428 Precondition Required (RFC 6585) so an older client fails loudly rather than corrupting quietly.
  2. Derive the ETag from something that changes on every write — a row version counter or a hash of the stored representation. A timestamp with one-second resolution fails when two writes land in the same second, which is the exact case you are defending against.

Why the other options fail

  • An updated_at field in the body is the same idea, hand-rolled, and it usually loses one of the two protections: it is optional (an old client omits it and overwrites), its resolution is too coarse, and it cannot be enforced by a gateway or a cache because it is not in the protocol. It is a reasonable fallback inside a private API with one client. It is not the mechanism.
  • PATCH narrows the blast radius and does not remove the race. Two agents patching the same field still lose one update, and merge-patch semantics add a trap of their own, where a null means "delete this field" rather than "leave it alone". Partial updates and concurrency control are orthogonal: you want both.
  • A database row lock held across an edit session means a lock held for the duration of a human's attention span, including lunch. It converts a rare silent conflict into routine blocking and a pile of abandoned transactions, and it costs you a connection per open form. Choose pessimistic locking only when the workflow is genuinely exclusive — a stock count, a payroll run — and then with an explicit lease of a few minutes that expires without anyone's help.

When not to bother

For an append-only resource, or an idempotent set-to-a-known-value write, there is nothing to lose and the ceremony buys nothing: the two lines of client code and the extra round trip after a 412 are a cost with no matching benefit. For a field where concurrent edits are semantically additive (a tag list, a counter), model the operation as the addition rather than the whole document, and conditional requests become unnecessary. And if you only have one writer, say so in the design note instead of building for a race that cannot happen.