DynamoDB Transactions in C#
Perform atomic, ACID-compliant transactions on DynamoDB using FluentDynamoDB. Transactions ensure that multiple operations across one or more tables either all succeed or all fail together, maintaining data consistency. This guide demonstrates write and read transactions using the three API patterns.
- Lambda/Fluent
- String Formatted
- Manual Builder
Write Transactions
// Transfer money between accounts atomically
await DynamoDbTransactions.Write
.Add(accountTable.Update(fromAccountId)
.Set(x => new AccountUpdate { Balance = x.Balance - amount })
.Where(x => x.Balance >= amount))
.Add(accountTable.Update(toAccountId)
.Set(x => new AccountUpdate { Balance = x.Balance + amount }))
.Add(transactionTable.Put(new Transaction
{
TransactionId = Guid.NewGuid().ToString(),
Amount = amount,
Timestamp = DateTime.UtcNow
}))
.ExecuteAsync();
Conditional Put
// Create order only if it doesn't exist
await DynamoDbTransactions.Write
.Add(orderTable.Put(order)
.Where(x => x.OrderId.AttributeNotExists()))
.Add(inventoryTable.Update(productId)
.Set(x => new InventoryUpdate { Quantity = x.Quantity - orderQuantity })
.Where(x => x.Quantity >= orderQuantity))
.ExecuteAsync();
ConditionCheck
// Verify inventory before confirming order
await DynamoDbTransactions.Write
.Add(inventoryTable.ConditionCheck(productId)
.Where(x => x.Quantity >= requiredQuantity))
.Add(orderTable.Update(orderId)
.Set(x => new OrderUpdate { Status = "confirmed" }))
.ExecuteAsync();
Read Transactions
// Get consistent snapshot of related data
var (user, account, order) = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(accountTable.Get(accountId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Account, Order>();
// Access results
Console.WriteLine($"User: {user?.Name}");
Console.WriteLine($"Balance: {account?.Balance}");
Console.WriteLine($"Order Status: {order?.Status}");
Read with Index Access
// Get response and access items by index
var response = await DynamoDbTransactions.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.Add(orderTable.Get("order1"))
.ExecuteAsync();
var user1 = response.GetItem<User>(0);
var user2 = response.GetItem<User>(1);
var order = response.GetItem<Order>(2);
Write Transactions
// Transfer money between accounts atomically
await DynamoDbTransactions.Write
.Add(accountTable.Update(fromAccountId)
.Set($"SET {Account.Fields.Balance} = {Account.Fields.Balance} - {{0}}", amount)
.Where($"{Account.Fields.Balance} >= {{0}}", amount))
.Add(accountTable.Update(toAccountId)
.Set($"SET {Account.Fields.Balance} = {Account.Fields.Balance} + {{0}}", amount))
.Add(transactionTable.Put(new Transaction
{
TransactionId = Guid.NewGuid().ToString(),
Amount = amount,
Timestamp = DateTime.UtcNow
}))
.ExecuteAsync();
Conditional Put
// Create order only if it doesn't exist
await DynamoDbTransactions.Write
.Add(orderTable.Put(order)
.Where($"attribute_not_exists({Order.Fields.OrderId})"))
.Add(inventoryTable.Update(productId)
.Set($"SET {Inventory.Fields.Quantity} = {Inventory.Fields.Quantity} - {{0}}", orderQuantity)
.Where($"{Inventory.Fields.Quantity} >= {{0}}", orderQuantity))
.ExecuteAsync();
ConditionCheck
// Verify inventory before confirming order
await DynamoDbTransactions.Write
.Add(inventoryTable.ConditionCheck(productId)
.Where($"{Inventory.Fields.Quantity} >= {{0}}", requiredQuantity))
.Add(orderTable.Update(orderId)
.Set($"SET {Order.Fields.Status} = {{0}}", "confirmed"))
.ExecuteAsync();
Read Transactions
// Get consistent snapshot of related data
var (user, account, order) = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(accountTable.Get(accountId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Account, Order>();
// Access results
Console.WriteLine($"User: {user?.Name}");
Console.WriteLine($"Balance: {account?.Balance}");
Console.WriteLine($"Order Status: {order?.Status}");
Read with Index Access
// Get response and access items by index
var response = await DynamoDbTransactions.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.Add(orderTable.Get("order1"))
.ExecuteAsync();
var user1 = response.GetItem<User>(0);
var user2 = response.GetItem<User>(1);
var order = response.GetItem<Order>(2);
Write Transactions
// Transfer money between accounts atomically
await DynamoDbTransactions.Write
.Add(accountTable.Update(fromAccountId)
.Set("SET #balance = #balance - :amount")
.WithAttribute("#balance", "balance")
.WithValue(":amount", amount)
.Where("#balance >= :minBalance")
.WithValue(":minBalance", amount))
.Add(accountTable.Update(toAccountId)
.Set("SET #balance = #balance + :amount")
.WithAttribute("#balance", "balance")
.WithValue(":amount", amount))
.Add(transactionTable.Put(new Transaction
{
TransactionId = Guid.NewGuid().ToString(),
Amount = amount,
Timestamp = DateTime.UtcNow
}))
.ExecuteAsync();
Conditional Put
// Create order only if it doesn't exist
await DynamoDbTransactions.Write
.Add(orderTable.Put(order)
.Where("attribute_not_exists(#pk)")
.WithAttribute("#pk", "pk"))
.Add(inventoryTable.Update(productId)
.Set("SET #qty = #qty - :orderQty")
.WithAttribute("#qty", "quantity")
.WithValue(":orderQty", orderQuantity)
.Where("#qty >= :minQty")
.WithValue(":minQty", orderQuantity))
.ExecuteAsync();
ConditionCheck
// Verify inventory before confirming order
await DynamoDbTransactions.Write
.Add(inventoryTable.ConditionCheck(productId)
.Where("#qty >= :required")
.WithAttribute("#qty", "quantity")
.WithValue(":required", requiredQuantity))
.Add(orderTable.Update(orderId)
.Set("SET #status = :status")
.WithAttribute("#status", "status")
.WithValue(":status", "confirmed"))
.ExecuteAsync();
Read Transactions
// Get consistent snapshot of related data
var (user, account, order) = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(accountTable.Get(accountId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Account, Order>();
// Access results
Console.WriteLine($"User: {user?.Name}");
Console.WriteLine($"Balance: {account?.Balance}");
Console.WriteLine($"Order Status: {order?.Status}");
Read with Index Access
// Get response and access items by index
var response = await DynamoDbTransactions.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.Add(orderTable.Get("order1"))
.ExecuteAsync();
var user1 = response.GetItem<User>(0);
var user2 = response.GetItem<User>(1);
var order = response.GetItem<Order>(2);
RequireWriteTransaction Attribute
For entities that must always be modified within transactions, use the [RequireWriteTransaction] attribute:
[DynamoDbTable("FinancialTransactions")]
[RequireWriteTransaction]
public partial class Transaction
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string TransactionId { get; set; } = string.Empty;
[DynamoDbAttribute("amount")]
public decimal Amount { get; set; }
[DynamoDbAttribute("timestamp")]
public DateTime Timestamp { get; set; }
}
When applied, non-transactional writes throw InvalidOperationException:
// ✅ Works - within transaction
await DynamoDbTransactions.Write
.Add(table.Transactions.Put(transaction))
.ExecuteAsync();
// ❌ Throws InvalidOperationException
await table.Transactions.Put(transaction).PutAsync();
// Error: "Entity 'Transaction' is marked with [RequireWriteTransaction] and cannot be
// modified outside of a transaction. Use DynamoDbTransactions.Write() to perform this operation."
Error Handling
try
{
await DynamoDbTransactions.Write
.Add(accountTable.Update(accountId)
.Set(x => new AccountUpdate { Balance = x.Balance - amount })
.Where(x => x.Balance >= amount))
.ExecuteAsync();
}
catch (TransactionCanceledException ex)
{
foreach (var reason in ex.CancellationReasons)
{
if (reason.Code == "ConditionalCheckFailed")
Console.WriteLine("Insufficient balance");
else if (reason.Code == "TransactionConflict")
Console.WriteLine("Concurrent modification - retry");
}
}
Learn More
- Transactions Guide - Complete transactions reference with advanced patterns
- Basic Operations - Single-item CRUD operations
- Batches - Non-atomic bulk operations
- Source Generation - RequireWriteTransaction attribute details