The benefit is immediate - an indicator of the number of times an entity has been modified which can be used for auditing the entity. Also, an additional use is for optimistic locking where an entity is allowed to be updated only if the holder updating it has the right version of the entity.
This post will go into details of how to introduce such a field with the DynamoDB related libraries of AWS SDK 2
Model
Consider a model called Hotel which is being persisted into a dynamo database. In Kotlin, it can be represented using the following data class:
data class Hotel( val id: String = UUID.randomUUID().toString(), val name: String, val address: String? = null, val state: String? = null, val zip: String? = null, val version: Long = 1L )A version field has been introduced in this model with an initial value of 1. The aim will be to save this field as-is and then let dynamo atomically manage the increment of this field at the point of saving this entity.
As the fields in this model gets changed, I would like the version to be updated along these lines:
Local version of DynamoDB
It is useful to have DynamoDB running on the local machine, this way not having to create real DynamoDB tables in AWS.
There are multiple ways of doing this. One is to use a docker version of DynamoDB Local, which can be started up the following way to listen on port 4569:
docker run -p 4569:8000 amazon/dynamodb-local:1.13My personal preference is to use localstack and the instructions at the site have different ways to start it up. I normally use docker-compose to bring it up. One of the reasons to use localstack over DynamoDB Local is that localstack provides a comprehensive set of AWS services for local testing and not just DynamoDB.
Quick Demo
I have the entire code available in my github repo here - https://github.com/bijukunjummen/boot-with-dynamodb
Once the application is brought up using the local version of dynamoDB, an entity can be created using the following httpie request:
http :9080/hotels id=4 name=name address=address zip=zip state=ORWith a response, where the version field is set to 1:
{ "address": "address", "id": "4", "name": "name", "state": "OR", "version": 1, "zip": "zip" }Then if the name is updated for the entity:
http PUT :9080/hotels/4 name=name1 address=address zip=zip state=OR version=1the version field gets updated to 2 and so on:
{ "address": "address", "id": "4", "name": "name1", "state": "OR", "version": 2, "zip": "zip" }Also note that if during an update a wrong version number is provided, the call would fail as there is an optimistic locking in place using this version field.
Implementing the version field
Implementing the version field depends on the powerful UpdateItem API provided by DynamoDB. One of the features of UpdateItem API is that it takes in a "UpdateExpression" which is a dsl which shows how different Dynamo attributes should be updated.
The raw request to AWS DynamoDB looks like this:
{ "TableName": "hotels", "Key": { "id": { "S": "1" } }, "UpdateExpression": "\nSET #name=:name,\n #state=:state,\naddress=:address,\nzip=:zip\nADD version :inc\n ", "ExpressionAttributeNames": { "#state": "state", "#name": "name" }, "ExpressionAttributeValues": { ":name": { "S": "testhotel" }, ":address": { "S": "testaddress" }, ":state": { "S": "OR" }, ":zip": { "S": "zip" }, ":inc": { "N": "1" } } }From the articles perspective, specifically focus on "ADD version :inc", which is an expression that tells AWS DynamoDB to increment the value of version by ":inc" value, which is provided separately using "ExpressionAttributeValues" with "1". Dealing with raw API in its json form is daunting, that is where the Software Development Kit(SDK) that AWS provides comes in, AWS SDK for Java 2 is a rewrite of AWS SDK's with a focus on using the latest Java features and Non-Blocking IO over the wire. Using AWS SDK for Java 2, an "UpdateItem" looks like this(using Kotlin code):
val updateItemRequest = UpdateItemRequest.builder() .tableName(TABLE_NAME) .key( mapOf( ID to AttributeValue.builder().s(hotel.id).build() ) ) .updateExpression( """ SET #name=:name, #state=:state, address=:address, zip=:zip ADD version :inc """ ) .conditionExpression("version = :version") .expressionAttributeValues( mapOf( ":${NAME}" to AttributeValue.builder().s(hotel.name).build(), ":${ZIP}" to AttributeValue.builder().s(hotel.zip).build(), ":${STATE}" to AttributeValue.builder().s(hotel.state).build(), ":${ADDRESS}" to AttributeValue.builder().s(hotel.address).build(), ":${VERSION}" to AttributeValue.builder().n(hotel.version.toString()).build(), ":inc" to AttributeValue.builder().n("1").build() ) ) .expressionAttributeNames( mapOf( "#name" to "name", "#state" to "state" ) ) .build() val updateItem: CompletableFuture<UpdateItemResponse> = dynamoClient.updateItem(updateItemRequest) return Mono.fromCompletionStage(updateItem) .flatMap { getHotel(hotel.id) }The highlighted line has the "Update Expression" with all the existing fields set to a new value and the version attribute incremented by 1. Another thing to note about this call is the "conditionExpression", which is essentially a way to tell DynamoDB to update the attributes if a condition matches up, in this specific instance if the existing value of version matches up. This provides a neat way to support optimistic locking on the record.
Conclusion
A lot of details here - the easiest way to get a feel for it is by trying out the code which is available in my github repository here - https://github.com/bijukunjummen/boot-with-dynamodb. The readme has good details on how to run it in a local environment.AWS DynamoDB provides a neat way to manage a version field on entities, ensuring that they are atomically updated and provides a way for them to used for optimistic locking