Transactions
For years, the trade-off of NoSQL was simple: you get massive scale, but you lose ACID transactions. If you needed to update two rows atomically (like transferring money), you used a relational database.
DynamoDB changed this with the introduction of Transactions. You can now perform atomic, all-or-nothing operations across multiple items—even if those items are in different tables.
1. The ACID Guarantee
DynamoDB transactions provide full ACID properties:
- Atomicity: All operations in the transaction succeed, or none of them do.
- Consistency: The data remains in a valid state.
- Isolation: Other operations cannot see the intermediate state of a transaction.
- Durability: Once committed, the changes are permanent.
2. Key API Operations
A. TransactWriteItems
This is a synchronous write operation that groups up to 100 individual actions (Put, Update, Delete, or ConditionCheck) into a single request.
- Cost: Consumes 2x WCU compared to standard writes.
- Use Case: Bank transfers, inventory reservation, order processing.
B. TransactGetItems
This is a synchronous read operation that retrieves up to 100 items.
- Cost: Consumes 2x RCU compared to standard reads.
- Isolation: Returns a consistent snapshot of all requested items. If a
TransactWriteis modifying these items,TransactGetwill either see all changes or none.
3. Interactive: The Atomic Transfer
Visualize how a transaction prevents data corruption. We will attempt to transfer $50 from Account A to Account B. We’ll simulate a failure where Account A has insufficient funds.
4. Code Implementation
Here is how to perform a bank transfer transaction. Notice the ConditionCheck: we ensure the sender has enough money. If this condition fails, the entire transaction is cancelled, and no money is added to the receiver.
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import java.util.HashMap;
import java.util.Map;
public class BankTransfer {
public static void main(String[] args) {
DynamoDbClient client = DynamoDbClient.create();
// 1. Define Deduct Action (Check Balance Condition)
Map<String, AttributeValue> deductKey = new HashMap<>();
deductKey.put("AccountId", AttributeValue.builder().s("ACC-A").build());
TransactWriteItem deductItem = TransactWriteItem.builder()
.update(Update.builder()
.tableName("Accounts")
.key(deductKey)
.updateExpression("SET Balance = Balance - :amount")
.conditionExpression("Balance >= :amount") // Critical Check
.expressionAttributeValues(Map.of(
":amount", AttributeValue.builder().n("50").build()
))
.build())
.build();
// 2. Define Credit Action
Map<String, AttributeValue> creditKey = new HashMap<>();
creditKey.put("AccountId", AttributeValue.builder().s("ACC-B").build());
TransactWriteItem creditItem = TransactWriteItem.builder()
.update(Update.builder()
.tableName("Accounts")
.key(creditKey)
.updateExpression("SET Balance = Balance + :amount")
.expressionAttributeValues(Map.of(
":amount", AttributeValue.builder().n("50").build()
))
.build())
.build();
// 3. Execute Transaction
try {
client.transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(deductItem, creditItem)
.build());
System.out.println("Transfer Successful!");
} catch (TransactionCanceledException e) {
System.err.println("Transaction Failed: " + e.cancellationReasons());
}
}
}
package main
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func main() {
cfg, _ := config.LoadDefaultConfig(context.TODO())
client := dynamodb.NewFromConfig(cfg)
// Execute Transaction
_, err := client.TransactWriteItems(context.TODO(), &dynamodb.TransactWriteItemsInput{
TransactItems: []types.TransactWriteItem{
{
Update: &types.Update{
TableName: aws.String("Accounts"),
Key: map[string]types.AttributeValue{"AccountId": &types.AttributeValueMemberS{Value: "ACC-A"}},
UpdateExpression: aws.String("SET Balance = Balance - :amount"),
// Critical Condition
ConditionExpression: aws.String("Balance >= :amount"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":amount": &types.AttributeValueMemberN{Value: "50"},
},
},
},
{
Update: &types.Update{
TableName: aws.String("Accounts"),
Key: map[string]types.AttributeValue{"AccountId": &types.AttributeValueMemberS{Value: "ACC-B"}},
UpdateExpression: aws.String("SET Balance = Balance + :amount"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":amount": &types.AttributeValueMemberN{Value: "50"},
},
},
},
},
})
if err != nil {
fmt.Printf("Transaction Failed: %v\n", err)
} else {
fmt.Println("Transfer Successful!")
}
}
[!WARNING] Idempotency: Always include a
ClientRequestToken(idempotency token) in your transaction requests. If a network error occurs and you retry the request, the token ensures the transaction isn’t executed twice (preventing double spending).
5. Limitations to Know
- Item Limit: Max 100 items per transaction.
- Size Limit: Total size of all items in the transaction cannot exceed 4 MB.
- Conflict: If another request modifies any of the items involved in your transaction while it is processing, your transaction will fail with
TransactionConflictException.