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 TransactWrite is modifying these items, TransactGet will 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.

Account A
$40
Account B
$100
> Transaction Log: Waiting for input...

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

  1. Item Limit: Max 100 items per transaction.
  2. Size Limit: Total size of all items in the transaction cannot exceed 4 MB.
  3. Conflict: If another request modifies any of the items involved in your transaction while it is processing, your transaction will fail with TransactionConflictException.