Optimistic locking is one of the concurrency control mechanism. It can help us to avoid situations when one user overrides changes made by another one. When 2 users wants to save the same object at the same time, we will reject one of them. Rejected user should review the new state of the object and decide if he still wants to do the update.
How to implement optimistic locking?
The implementation mainly comes down to introducing additional information about the current version of the object. Each time you save it you should:
- check if the current version on which we made the changes is the one saved in the database (if not, stop saving)
- generate an identifier for the next version and save it with the object
In practice, in the case of relational databases, it comes down to making an update with an additional condition with a version identifier, and check the number of modified records as a result of query execution (if 0, it means we have a conflict):
UPDATE my_table SET my_content = ? WHERE id = ? AND obj_version = ?
To identify the version of the record you can use e.g. number, date, string (e.g. generated UUID), it is important that the generated values are unique within a given object.
When (not) to use it?
- Use it when many users may update the same object at the same time, and the state of object is important during processing.
- Don’t use it when object may be modified very often by many users (high rejection rate).
- Don’t use it when you do not care about checking the previous state of the object (when the state of the object does not affect the possibility of updating).
How to use it?
The optimistic locking can be used for the saving process itself, or for the entire editing process.
Two users have started processing the order placed in the online store. At the same time, they executed 2 different operations. The first one decided to cancel the order and the second one decided to approve it. It could look like this:
If we do not use the blocking mechanism here, there may be a problem - a cancelled order, which has already started the process of returning the money to the customer, will be approved and will eventually end up with the customer receiving a refund and purchased product.
In this case, it is sufficient for us to apply blocking only in the area of saving the data itself. If we detect that something has modified its state between the order reading and writing inside the process (the order version ID from read does not match during write), we will throw an optimistic lock exception.
In this case, we can even automatically retry the rejected process - the 2nd approach to processing the request will no longer pass the validation stage - validation will return an error that the order has been cancelled so it can no longer be approved.
But locking only in the area of the saving process itself, can often be insufficient and you will also need to return the object’s version to the user interface.
Two employees want to modify the product - a blue L size T-shirt. One of them wants to change the color to green, the 2nd wants to change the size to M. If there are 2 separate actions in our system, the object’s version should also be loaded into the user interface.
The above sample flow without the object's version may lead to a non-existent product - a green T-shirt of size M - something we don't have in the store and probably nobody will notice the problem until the first order. In the case of using the optimistic locking with provided version of the object to the frontend, it can protect us from this. At the time of saving we can verify that the customer wanted to change the size when the product was in a different version (because of changed color) and inform him about this fact.
Auto-retry or not?
Very often silent repetition is not a good solution. On the one hand, we can prevent the user from displaying a saving error, but on the other hand, we can lead to an incorrect state being saved.
It all depends on the business domain but very often such a retry can lead to similar problems as in the lack of locking. In most cases we shouldn't retry, but inform the user that the object he is working on has changed in the meantime.