Amazon DynamoDB is a fully managed NoSQL database service that offers high performance, scalability, and availability for any application. One of its key features that sets it apart from other NoSQL databases is its support for transactions.
Transactions in DynamoDB are a crucial mechanism for ensuring data consistency and integrity in your applications. They allow you to perform multiple operations on multiple items within a single atomic unit, guaranteeing that either all operations are successful or none of them are.
The two main types of operations supported by DynamoDB transactions are TransactWriteItems
and TransactGetItems
.
The complete code for this project is available on my GitHub repository (getButton) #text=(GitHub) #icon=(share) #color=(#000000)
Dependencies
ext {
springCloudAwsVersion = '3.0.0'
}
dependencies {
// spring cloud aws BOM(Bill of materials)
implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudAwsVersion}")
// dynamoDB
implementation 'io.awspring.cloud:spring-cloud-aws-starter-dynamodb'
}
Application Configuration (application.properties)
# DynamoDB connection
spring.cloud.aws.credentials.access-key=
spring.cloud.aws.credentials.secret-key=
# Choose your region according to your table. Because I have a DynamoDB table in the ap-south-1 area, I've set the endpoint and region.
spring.cloud.aws.region.static=ap-south-1
spring.cloud.aws.endpoint=https://dynamodb.ap-south-1.amazonaws.com
Want to learn more about efficient data management in DynamoDB? My tutorial on (getButton) #text=(DynamoDB BatchWriteItem and BatchGetItem in Java Spring Boot Applications) #icon=(link) #color=(#35a576) is a great starting point.(alert-passed)
TransactWriteItems API
TransactWriteItems
is a synchronous, idempotent operation that allows you to group up to 100 write actions into a single atomic transaction, ensuring either complete success or failure.
These actions can target up to 100 distinct items across multiple tables within the same region.The aggregate size of the items in the transaction cannot exceed 4 MB
Unlike BatchWriteItem operations, TransactWriteItems require all included actions to be completed successfully for the entire transaction to be considered successful. With BatchWriteItem, individual actions can succeed or fail independently.(alert-warning)
You can't target the same item with multiple operations within the same transaction. For example, you can't perform a ConditionCheck and also an Update action on the same item in the same transaction.
software.amazon.awssdk.services.dynamodb.model.DynamoDbException: Transaction request cannot include multiple operations on one item(alert-error)
To illustrate the practical application of all four TransactWriteItems operations, we'll use an e-commerce scenario. I have four tables: customer, orders, products, and cart, and we'll simulate a customer placing an order.
ConditionCheck: Ensuring Data Integrity with Conditional Writes
ConditionCheck
Determines if an item exists or meets certain criteria.
When we need to determine if a certain condition is true, it's often best to use condition checking. For example, before allowing a customer to place an order, I'll check if their account is active.
private ConditionCheck customerExistsConditionCheck() {
// customer table partition key and sort key
HashMap<String, AttributeValue> customerKey = new HashMap<>();
customerKey.put("customer_id", AttributeValue.fromS("c_123")); // partition key
customerKey.put("email", AttributeValue.fromS("dummy@email.com")); // sort key
// expression attribute values is fill the expected values in the query placeholder.
Map<String, AttributeValue> customerExpressionAttributeValue = new HashMap<>();
customerExpressionAttributeValue.put(":expected_active_status", AttributeValue.fromBool(true));
// verify attribute 'active' exist in the items and active status must be true.
String attribute = "active";
return ConditionCheck.builder()
.tableName("customer")
.key(customerKey)
.conditionExpression("attribute_exists(" + attribute + ")")
.conditionExpression("active = :expected_active_status")
.expressionAttributeValues(customerExpressionAttributeValue)
.build();
}
tableName() method
The tableName method in the ConditionCheck class of the AWS SDK for Java is primarily used to specify the DynamoDB table that you want to perform the conditional operation on
key() method
The ConditionCheck class's key method accepts a partition key and a sort key as parameters.
conditionExpression() method
The conditionExpression method in the ConditionCheck class of the AWS SDK for Java is used to specify a logical expression that must be satisfied for a DynamoDB operation to succeed.
The conditionExpression is typically combined with withExpressionAttributeValues and withExpressionAttributeNames to define the specific attributes and values to be used in the expression.
expressionAttributeValues() method
The expressionAttributeValues method in the ConditionCheck class of the AWS SDK is used to define the actual values that will be used in the condition expression.
When you create a condition expression, you often use placeholders for attribute values. The expressionAttributeValues method allows you to map these placeholders to their corresponding actual values. This ensures that the condition expression is evaluated correctly with the correct data.
Functions in DynamoDB
attribute_exists
: True if the item contains the attribute specified by path.
Example: Check whether an item in the Product table has a side view picture.
attribute_exists (#Pictures.#SideView)
To explore DynamoDB functions in depth, check out Condition and filter expressions, operators, and functions
Put: Adding or Updating Items
Put
Initiates a PutItem operation to create a new item or update an existing one, either unconditionally or based on specified conditions
private Put placeOrder() {
// create a map with key value pair to insert a record into order table
Map<String, AttributeValue> orderItems = new HashMap<>();
orderItems.put("order_id", AttributeValue.fromS("od_123")); // partition key
orderItems.put("product_id", AttributeValue.fromS("p_123")); // sort key
orderItems.put("customer_id", AttributeValue.fromS("c_123"));
orderItems.put("total_amount", AttributeValue.fromS("101"));
orderItems.put("status", AttributeValue.fromS("CONFIRM"));
return Put.builder()
.tableName("orders")
.item(orderItems)
// An optional parameter that returns the item attributes for an UpdateItem operation that failed a condition check.
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build();
}
item() method
item
: The items method takes a Map<String, AttributeValue> as an argument. This map represents the attributes and their corresponding values that you want to associate with the item.
• Key: The key in the map represents the attribute name.
• Value: The value is an AttributeValue object that contains the actual value for the attribute. This value can be of various types, such as a string, number, or list.
ReturnValuesOnConditionCheckFailure() method
ReturnValuesOnConditionCheckFailure
: The ReturnValuesOnConditionCheckFailure parameter determines what DynamoDB should do when a conditional write fails. Typically, if a condition isn't met, DynamoDB will only report an error.
However, with ReturnValuesOnConditionCheckFailure, you can choose to have DynamoDB return the item's original attributes before the write attempt failed. You have two options:
• ALL_OLD: Returns all attributes of the item.
• NONE: Returns no data (the default).
Update: Modifying Existing Items
Update
Performs an UpdateItem operation to modify an existing item or create a new one if it doesn't exist. This action allows you to conditionally add, delete, or update attributes.
private Update updateProductQuantity() {
// products table partition key and sort key
HashMap<String, AttributeValue> productsKey = new HashMap<>();
productsKey.put("product_id", AttributeValue.fromS("p_123")); // partition key
productsKey.put("name", AttributeValue.fromS("laptop")); // sort key
// expression attribute values is fill the expected values in the query placeholder.
Map<String, AttributeValue> productsExpressionAttributeValue = new HashMap<>();
productsExpressionAttributeValue.put(":available_quantity", AttributeValue.fromN("11"));
productsExpressionAttributeValue.put(":expected_product_status", AttributeValue.fromBool(true));
return Update.builder()
.tableName("products")
.key(productsKey)
// query - set quantity to 11
.updateExpression("SET quantity = :available_quantity")
// query - just like a where clause
.conditionExpression("product_status = :expected_product_status")
// provide actual values of placeholder used in the query i.e., available_quantity and expected_product_status
.expressionAttributeValues(productsExpressionAttributeValue)
// An optional parameter that returns the item attributes for an UpdateItem operation that failed a condition check.
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build();
}
updateExpression() method
The updateExpression
method in the Update class of the AWS SDK for DynamoDB is used to specify how you want to modify an existing item in a table. It allows you to:
• Set new values for attributes.
• Add values to sets or numbers.
• Remove attributes or elements from sets.
• Delete elements from lists or maps
You can use placeholders in your updateExpression and then define the corresponding values using the expressionAttributeValues
method. This provides a flexible way to update items based on specific conditions or requirements.
Delete: Removing Items from the Table
Delete
Performs a DeleteItem operation on a specific item within a table based on its primary key.
private Delete deleteCartEntry() {
// cart table partition key and sort key
HashMap<String, AttributeValue> cartKey = new HashMap<>();
cartKey.put("cart_id", AttributeValue.fromS("ct_123")); // partition key
cartKey.put("product_id", AttributeValue.fromS("p_123")); // sort key
// expression attribute values is fill the expected values in the query placeholder.
Map<String, AttributeValue> cartExpressionAttributeValue = new HashMap<>();
cartExpressionAttributeValue.put(":expected_quantity", AttributeValue.fromN("1"));
return Delete.builder()
.tableName("cart")
.key(cartKey)
// query - just like where clause, here verifying the quantity is equal to 1
.conditionExpression("quantity = :expected_quantity")
// provide actual values of placeholder used in the query i.e expected_quantity which is 1
.expressionAttributeValues(cartExpressionAttributeValue)
// An optional parameter that returns the item attributes for an UpdateItem operation that failed a condition check.
.returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
.build();
}
Within the AWS SDK for Java, the ConditionCheck, Put, Update, and Delete classes found in the software.amazon.awssdk.services.dynamodb.model package.(alert-success)
Executing the Transaction
Executing transactions in DynamoDB using TransactionWriteItem ensures atomic operations across multiple items.
• Create a list of TransactWriteItem
objects: Each object defines a single write operation (put, update, or delete) on an item.
• Execute the transaction: Call the TransactWriteItems API, passing in the list of TransactWriteItemsRequest objects.
public void transactionWrite() {
// Collect Transaction Write in a list
List<TransactWriteItem> transactionWriteItem = List.of(TransactWriteItem.builder().conditionCheck(customerExistsConditionCheck()).build(),
TransactWriteItem.builder().put(placeOrder()).build(),
TransactWriteItem.builder().update(updateProductQuantity()).build(),
TransactWriteItem.builder().delete(deleteCartEntry()).build());
// TransactWriteItemRequest
TransactWriteItemsRequest transactWriteItemsRequest = TransactWriteItemsRequest.builder()
.transactItems(transactionWriteItem)
.build();
// Run the transaction write item
try
{
TransactWriteItemsResponse transactWriteItemsResponse = dynamoDbClient.transactWriteItems(transactWriteItemsRequest);
System.out.println("Successfully completed write operation");
} catch (Exception exception) {
exception.printStackTrace();
}
}
ATransactionCanceledException
exception may occur due to any of the following reasons
• When a condition in one of the condition expressions is not met.
• When a transaction validation error occurs because more than one action in the same TransactWriteItems operation targets the same item.
• When aTransactWriteItems
request conflicts with an ongoingTransactWriteItems
operation on one or more items in theTransactWriteItems
request.(alert-error)
TransactGetItems API
TransactGetItems
efficiently fetches data from multiple DynamoDB tables by combining up to 100 individual Get operations into a single synchronous request.
This allows for retrieving data from a maximum of 100 items across different tables within the same AWS account and Region, provided the total data size doesn't exceed 4 MB.
The Get
actions are performed atomically so that either all of them succeed or all of them fail:
Get
— Initiates a GetItem operation to retrieve a set of attributes for the item with the given primary key. If no matching item is found, Get does not return any data.
To illustrate the TransactGetItems operation, I will retrieve data from both the customer and orders tables in the following example.
public void transactionRead() {
// customer's partition key and sort key
HashMap<String, AttributeValue> customerKey = new HashMap<>();
customerKey.put("customer_id", AttributeValue.fromS("c_123")); // partition Key
customerKey.put("email", AttributeValue.fromS("dummy@email.com")); // sort key
// order's partition key and sort key
Map<String, AttributeValue> orderKey = new HashMap<>();
orderKey.put("order_id", AttributeValue.fromS("od_123")); // partition Key
orderKey.put("product_id", AttributeValue.fromS("p_123")); // sort key
// Fetch all the customer whose partition key and sort key match
Get customersGet = Get.builder()
.tableName("customer")
.key(customerKey)
.build();
// Fetch all the orders whose partition key and sort key match
Get ordersGet = Get.builder()
.tableName("orders")
.key(orderKey)
.build();
// Collect all the read operation in a list of TransactGetItem
List<TransactGetItem> transactGetItemList = List.of(TransactGetItem.builder().get(customersGet).build(),
TransactGetItem.builder().get(ordersGet).build());
// TransactGetItemRequest
TransactGetItemsRequest transactGetItemsRequest = TransactGetItemsRequest.builder()
.transactItems(transactGetItemList)
.returnConsumedCapacity(ReturnConsumedCapacity.TOTAL)
.build();
// Run the transactGetItem operation
try {
TransactGetItemsResponse transactGetItemsResponse = dynamoDbClient.transactGetItems(transactGetItemsRequest);
List<ItemResponse> responses = transactGetItemsResponse.responses();
if (Objects.nonNull(responses) && !responses.isEmpty()) {
responses.stream()
.flatMap(response -> response.item().entrySet().stream())
.forEach(entry -> {
System.out.println("attribute: " + entry.getKey() + " value: " + entry.getValue());
} );
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
Read transactions are unsuccessful in the following cases:
• When aTransactGetItems
request conflicts with an ongoingTransactWriteItems
operation on one or more items in theTransactGetItems
request. In this case, the request fails with a TransactionCanceledException.(alert-error)
Conclusion: Leveraging TransactWriteItems and TransactGetItems for Robust DynamoDB Transactions
This article has demonstrated the effective use of TransactWriteItems and TransactGetItems to execute complex transactions within DynamoDB.
By understanding the core concepts, best practices, and potential limitations, developers can confidently implement atomic operations to maintain data integrity and consistency in their applications.
Enhance your DynamoDB table's performance and lower operational costs by implementing (getButton) #text=(DynamoDB's Time to Live for Automatic Data Deletion) #icon=(link) #color=(#35a576) feature.
The complete code for this project is available on my GitHub repository (getButton) #text=(GitHub) #icon=(share) #color=(#000000)
Keep learning and keep growing.