I have a serverless application using APIGateway, Lambda, and DynamoDB with NodeJS and typescript. I have a handful of situations were multiple clients could attempt to modify certain resources simultaneously. These race conditions usually involve querying a record, making a decision on how it should be modified, and then running an update. Because of this, I have applied opportunistic locking via conditional expressions, and I use Date.now() as the version. Thus, I query the record, make a decision about how it should be modified, and if the conditional check fails, I can simply query the record again and make a potentially different decision. Opportunistic locking is a very natural solution.
However, there is one racing situation where I need to update two records at the same time. I query the two records, decide how I should modify them, and then run updates. However, if either were updated by a second client during that time, the first client should query the two records again and potentially make a different decision.
I solved this problem using a transaction with two update operations. Each one included its own separate conditional expression so that if either one failed, both would fail in an all or nothing fashion.
It’s probably a decent solution, but then I read some DynamoDB documentation and it said that transactions can fail in the following circumstance:
When a TransactWriteItems request conflicts with an ongoing TransactWriteItems operation on one or more items in the TransactWriteItems request. In this case, the request fails with a TransactionCanceledException.
From what I understand in the rest of the article, if two transactions involve the same record, one of the transactions will fail with an exception. I understand that this is intended to address race conditions where the failing client can catch the exception and try again.
My question is whether implementing opportunistic locking for transactions is completely redundant, whether catching the TransactionCanceledException is sufficient to handle my race conditions. Or could there be some value in having redundancy? After all, what if I query my two records and then some other transaction modifies them in the few milliseconds before I can even start my transaction?