Two saves, one cart, and one of them is lost
Two API calls fired in parallel, each writing a different field on the same cart. One field kept coming back empty, and the cause is in how Magento builds its UPDATE.
A field on the cart kept ending up empty. Not always. Maybe one time in three, and never when anyone was watching closely enough to catch it.
The front end fired two mutations at once, in a single Promise.all. One set an identifier on the quote. The other saved some custom field data on the same quote. Different fields, different mutations, no obvious reason for them to interfere.
The cause
Magento does not write the column you changed. It writes all of them.
cartRepository->save($quote) builds its UPDATE from the whole in-memory object, because AbstractDb::prepareDataForUpdate serialises every field it is holding rather than tracking which ones you touched. So the sequence is the classic lost update:
Request A loads the quote. Field X is null, field Y is null.
Request B loads the quote. Same state.
Request A sets X and saves. The row now has X, and Y is still null.
Request B sets Y and saves, writing the whole row it loaded, in which X was null. X is gone.
Whichever commits last wins, and it wins with data it read before the other one committed. Nothing errors. Both mutations return success, because from each request's point of view the save worked perfectly.
The part that made it hard to reproduce
I tried to write a test for it in a single process and could not make it fail. The repository caches loaded quotes by id, so calling get() twice hands you the same object, and both "requests" then mutate one shared instance. The bug vanishes precisely because the test is not doing what production does.
You have to force two separate repository instances to see it. That is worth knowing generally: an object cache in a repository will hide every concurrency bug you try to reproduce through that repository.
What to do about it
The reflex is a lock or a retry. Usually there is something simpler available. In this case the two writes did not need to be concurrent at all. One of them could happen earlier, on the same row, before the other request existed, which made it atomic by construction rather than by coordination.
When that is not available, the options are the ordinary ones: serialise the calls, write both fields in one mutation, or take a row lock. What you cannot do is assume that writing different fields of the same row is safe. In Magento it is not.
