Hi @Egbert-Jan_Terpstra welcome to the community!
MongoDB using WiredTiger has document-level locking since at least MongoDB 3.0, so there will be no instant where the document is partly updated (unless your app runs multiple update commands that deliberately only update parts of a document).
From my understanding, this could go wrong because if this user would make two rapid requests after each other or another user makes a request after the retrieval of the document the data will be old and invalid in which case the document will not have the new data and data may get corrupted.
If I understand correctly, your workflow involves: 1) retrieving a document, 2) calculate new values for that document based on the document’s current state, then 3) push an updated document, and you’re worried that in-between steps 1 and 3, there are other threads that are trying to update the exact same document. Is this accurate?
If yes, using a transaction is a perfectly valid solution. Having a field called locked to act as a semaphore is also valid, although you’ll need to take care of cases where an update crashes between steps 1 & 3, rendering the document locked forever until it’s manually unlocked. From the application side, it is also possible to implement the semaphore construct outside of the database, to ensure that no two threads are working on the same document at one time.
In my opinion, the simplest & safest solution is to use transaction. Yes you’ll have to deploy a replica set (three data-bearing nodes as the minimum recommended for production), but the benefits are many. For example, you’ll gain high availability, redundancy and thus safer data storage, easier database maintenance, while also gaining transactions, change streams, and other replica-set specific features. In fact, I would not recommend you to deploy a standalone node for a production environment, ever. Using transactions, you would not need to do locked documents cleanup, nor have to implement special semaphore in your app.
Best regards
Kevin