Transactions
DynamoDB transactions provide ACID (Atomicity, Consistency, Isolation, Durability) guarantees for multiple operations across one or more tables. All operations in a transaction succeed together or fail together, ensuring data consistency.
Overview
DynamoDB supports two types of transactions:
TransactWriteItems:
- Put, Update, Delete, and ConditionCheck operations
- Up to 100 unique items or 4MB of data
- All operations succeed or all fail atomically
- Supports conditional expressions
TransactGetItems:
- Get operations with snapshot isolation
- Up to 100 unique items or 4MB of data
- All reads occur at the same point in time
- Provides consistent view across items
Quick Start
The transaction API uses static entry points and reuses existing request builders:
using Amazon.DynamoDBv2;
using Oproto.FluentDynamoDb;
var client = new AmazonDynamoDBClient();
var userTable = new UsersTable(client, "users");
var orderTable = new OrdersTable(client, "orders");
// Write transaction - all operations succeed or fail together
await DynamoDbTransactions.Write
.Add(userTable.Put(user))
.Add(orderTable.Update(orderId).Set(x => new { Status = "confirmed" }))
.ExecuteAsync();
// Read transaction with tuple destructuring
var (user, order) = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Order>();
Write Transactions
Write transactions allow you to perform multiple write operations atomically using the DynamoDbTransactions.Write entry point.
Put Operations
Put operations create new items or replace existing items:
// Lambda Expression (Recommended) - with condition
await DynamoDbTransactions.Write
.Add(userTable.Put(user).Where(x => x.UserId.AttributeNotExists()))
.ExecuteAsync();
// Format String (Alternative)
await DynamoDbTransactions.Write
.Add(userTable.Put(user).Where("attribute_not_exists(pk)"))
.ExecuteAsync();
Update Operations
Update operations modify existing items using the fluent update builder:
// Lambda Expression (Recommended)
await DynamoDbTransactions.Write
.Add(userTable.Update(userId).Set(x => new UserUpdateModel
{
Name = "Jane Doe",
UpdatedAt = DateTime.UtcNow
}))
.ExecuteAsync();
// Format String (Alternative)
await DynamoDbTransactions.Write
.Add(userTable.Update(userId).Set("name = {0}, updatedAt = {1:o}",
"Jane Doe", DateTime.UtcNow))
.ExecuteAsync();
With Conditions:
// Lambda Expression (Recommended)
await DynamoDbTransactions.Write
.Add(accountTable.Update(accountId)
.Set(x => new UpdateModel
{
Balance = x.Balance - 100.00m,
UpdatedAt = DateTime.UtcNow
})
.Where(x => x.Balance >= 100.00m))
.ExecuteAsync();
// Format String (Alternative)
await DynamoDbTransactions.Write
.Add(accountTable.Update(accountId)
.Set("balance = balance - {0:F2}, updatedAt = {1:o}",
100.00m, DateTime.UtcNow)
.Where("balance >= {0:F2}", 100.00m))
.ExecuteAsync();
Delete Operations
Delete operations remove items:
// Simple delete
await DynamoDbTransactions.Write
.Add(userTable.Delete(userId))
.ExecuteAsync();
// Delete with composite key
await DynamoDbTransactions.Write
.Add(orderTable.Delete("customer123", "order456"))
.ExecuteAsync();
// Lambda Expression (Recommended) - conditional delete
await DynamoDbTransactions.Write
.Add(userTable.Delete(userId).Where(x => x.Status == "inactive"))
.ExecuteAsync();
// Format String (Alternative)
await DynamoDbTransactions.Write
.Add(userTable.Delete(userId).Where("status = {0}", "inactive"))
.ExecuteAsync();
ConditionCheck Operations
Condition checks verify conditions without modifying data. Use them to validate state before proceeding with other operations:
// Lambda Expression (Recommended)
await DynamoDbTransactions.Write
.Add(inventoryTable.ConditionCheck(productId)
.Where(x => x.Quantity >= requiredQuantity))
.Add(orderTable.Update(orderId).Set(x => new { Status = "confirmed" }))
.ExecuteAsync();
// Format String (Alternative)
await DynamoDbTransactions.Write
.Add(inventoryTable.ConditionCheck(productId)
.Where("quantity >= {0}", requiredQuantity))
.Add(orderTable.Update(orderId).Set("status = {0}", "confirmed"))
.ExecuteAsync();
Use Case: Verify inventory before confirming an order. If inventory is insufficient, the entire transaction fails.
Read Transactions
Read transactions provide snapshot isolation, ensuring all reads occur at the same point in time.
Basic Read Transaction
// Execute and get response wrapper
var response = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(accountTable.Get(accountId))
.ExecuteAsync();
// Deserialize items by index
var user = response.GetItem<User>(0);
var account = response.GetItem<Account>(1);
ExecuteAndMapAsync - Tuple Destructuring
For convenience with small numbers of items, use ExecuteAndMapAsync to get a tuple:
// 2 items
var (user, order) = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Order>();
// 3 items
var (user, account, order) = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(accountTable.Get(accountId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Account, Order>();
// Up to 8 items supported
var (item1, item2, item3, item4, item5, item6, item7, item8) =
await DynamoDbTransactions.Get
.Add(table1.Get(key1))
.Add(table2.Get(key2))
// ... up to 8 items
.ExecuteAndMapAsync<T1, T2, T3, T4, T5, T6, T7, T8>();
Response Deserialization Methods
The TransactionGetResponse provides multiple ways to deserialize items:
var response = await DynamoDbTransactions.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.Add(userTable.Get("user3"))
.Add(orderTable.Get("order1"))
.ExecuteAsync();
// Get single item by index
var user1 = response.GetItem<User>(0);
// Get multiple items of same type by indices
var users = response.GetItems<User>(0, 1, 2);
// Get contiguous range of items
var allUsers = response.GetItemsRange<User>(0, 2); // indices 0, 1, 2
// Get item from different table
var order = response.GetItem<Order>(3);
// Check total count
Console.WriteLine($"Retrieved {response.Count} items");
Handling Null Items
Items that don't exist return null:
var response = await DynamoDbTransactions.Get
.Add(userTable.Get("user123"))
.Add(userTable.Get("nonexistent"))
.ExecuteAsync();
var user1 = response.GetItem<User>(0); // Returns User object
var user2 = response.GetItem<User>(1); // Returns null
if (user2 == null)
{
Console.WriteLine("User not found");
}
Read Transaction Across Multiple Tables
public async Task<UserSnapshot> GetUserSnapshot(string userId, string accountId)
{
var (user, account, recentOrder) = await DynamoDbTransactions.Get
.Add(userTable.Get(userId))
.Add(accountTable.Get(accountId))
.Add(orderTable.Get(userId, "ORDER#LATEST"))
.ExecuteAndMapAsync<User, Account, Order>();
// All items are read at the same point in time
return new UserSnapshot
{
User = user,
Account = account,
RecentOrder = recentOrder
};
}
Use Case: Get a consistent snapshot of user data, account balance, and recent orders at the exact same moment.
Complete Transaction Examples
Money Transfer
A classic example of when transactions are essential - transferring money between accounts:
public async Task TransferMoney(
string fromAccountId,
string toAccountId,
decimal amount)
{
try
{
await DynamoDbTransactions.Write
// Debit from account
.Add(accountTable.Update(fromAccountId)
.Set(x => new UpdateModel
{
Balance = x.Balance - amount,
UpdatedAt = DateTime.UtcNow
})
.Where(x => x.Balance >= amount))
// Credit to account
.Add(accountTable.Update(toAccountId)
.Set(x => new UpdateModel
{
Balance = x.Balance + amount,
UpdatedAt = DateTime.UtcNow
}))
// Create transaction record
.Add(transactionTable.Put(new Transaction
{
TransactionId = Guid.NewGuid().ToString(),
FromAccount = fromAccountId,
ToAccount = toAccountId,
Amount = amount,
Timestamp = DateTime.UtcNow
}))
.ExecuteAsync();
Console.WriteLine("Transfer successful");
}
catch (TransactionCanceledException ex)
{
Console.WriteLine($"Transfer failed: {ex.Message}");
foreach (var reason in ex.CancellationReasons)
{
Console.WriteLine($"Reason: {reason.Code} - {reason.Message}");
}
}
}
Order Processing with Inventory Check
public async Task ProcessOrder(Order order, List<OrderItem> items)
{
var transaction = DynamoDbTransactions.Write;
// Create order
transaction.Add(orderTable.Put(order)
.Where(x => x.OrderId.AttributeNotExists()));
// Check and update inventory for each item
foreach (var item in items)
{
// Check inventory availability
transaction.Add(inventoryTable.ConditionCheck(item.ProductId)
.Where(x => x.Quantity >= item.Quantity));
// Decrement inventory
transaction.Add(inventoryTable.Update(item.ProductId)
.Set(x => new { Quantity = x.Quantity - item.Quantity }));
// Create order item record
transaction.Add(orderItemTable.Put(item));
}
try
{
await transaction.ExecuteAsync();
Console.WriteLine("Order processed successfully");
}
catch (TransactionCanceledException ex)
{
Console.WriteLine("Order processing failed - insufficient inventory or order already exists");
}
}
Error Handling
TransactionCanceledException
The most common exception when a transaction fails:
using Amazon.DynamoDBv2.Model;
try
{
await DynamoDbTransactions.Write
.Add(accountTable.Update(accountId)
.Set(x => new { Balance = x.Balance - 100.00m })
.Where(x => x.Balance >= 100.00m))
.ExecuteAsync();
}
catch (TransactionCanceledException ex)
{
Console.WriteLine($"Transaction failed: {ex.Message}");
// Check cancellation reasons
foreach (var reason in ex.CancellationReasons)
{
Console.WriteLine($"Code: {reason.Code}");
Console.WriteLine($"Message: {reason.Message}");
// Common codes:
// - ConditionalCheckFailed: Condition expression failed
// - ItemCollectionSizeLimitExceeded: Item collection too large
// - TransactionConflict: Concurrent transaction conflict
// - ProvisionedThroughputExceeded: Capacity exceeded
// - ValidationError: Invalid request
}
}
Handling Specific Failure Reasons
try
{
await DynamoDbTransactions.Write
.Add(accountTable.Update(accountId)
.Set(x => new { Balance = x.Balance - amount })
.Where(x => x.Balance >= amount))
.ExecuteAsync();
}
catch (TransactionCanceledException ex)
{
var hasConditionalCheckFailure = ex.CancellationReasons
.Any(r => r.Code == "ConditionalCheckFailed");
if (hasConditionalCheckFailure)
{
Console.WriteLine("Insufficient balance");
}
var hasConflict = ex.CancellationReasons
.Any(r => r.Code == "TransactionConflict");
if (hasConflict)
{
Console.WriteLine("Transaction conflict - retry with exponential backoff");
}
}
Retry Strategy with Exponential Backoff
public async Task ExecuteTransactionWithRetry(int maxRetries = 3)
{
for (int i = 0; i < maxRetries; i++)
{
try
{
await DynamoDbTransactions.Write
.Add(accountTable.Update(fromAccountId)
.Set(x => new { Balance = x.Balance - amount })
.Where(x => x.Balance >= amount))
.Add(accountTable.Update(toAccountId)
.Set(x => new { Balance = x.Balance + amount }))
.ExecuteAsync();
return; // Success
}
catch (TransactionCanceledException ex)
{
var hasConflict = ex.CancellationReasons
.Any(r => r.Code == "TransactionConflict");
if (hasConflict && i < maxRetries - 1)
{
// Exponential backoff: 100ms, 200ms, 400ms
var delayMs = 100 * (int)Math.Pow(2, i);
await Task.Delay(delayMs);
continue;
}
throw;
}
}
}
Best Practices
Use Transactions for ACID Requirements
// ✅ Good - use transactions for atomic operations
await DynamoDbTransactions.Write
.Add(accountTable.Update(fromAccount)
.Set(x => new { Balance = x.Balance - amount }))
.Add(accountTable.Update(toAccount)
.Set(x => new { Balance = x.Balance + amount }))
.ExecuteAsync();
// ❌ Avoid - separate operations can leave inconsistent state
await accountTable.Update(fromAccount).Set(...).UpdateAsync();
await accountTable.Update(toAccount).Set(...).UpdateAsync();
Use Lambda Expressions for Type Safety
// ✅ Recommended - compile-time type checking
await DynamoDbTransactions.Write
.Add(userTable.Update(userId)
.Set(x => new UpdateModel { Name = "John", Age = 30 }))
.ExecuteAsync();
// ⚠️ Alternative - works but less type-safe
await DynamoDbTransactions.Write
.Add(userTable.Update(userId)
.Set("name = {0}, age = {1}", "John", 30))
.ExecuteAsync();
Use ConditionCheck for Validation
// ✅ Good - verify conditions before modifying data
await DynamoDbTransactions.Write
.Add(inventoryTable.ConditionCheck(productId)
.Where(x => x.Quantity >= requiredQuantity))
.Add(orderTable.Update(orderId).Set(x => new { Status = "confirmed" }))
.ExecuteAsync();
Use Client Request Tokens for Idempotency
// ✅ Good - prevents duplicate transactions on retry
var requestToken = Guid.NewGuid().ToString();
await DynamoDbTransactions.Write
.WithClientRequestToken(requestToken)
.Add(userTable.Put(user))
.ExecuteAsync();
Keep Transactions Small
Transactions consume 2x the capacity of standard operations. Keep them focused:
// ✅ Good - small, focused transaction
await DynamoDbTransactions.Write
.Add(userTable.Put(user))
.Add(auditTable.Put(audit))
.ExecuteAsync();
// ❌ Avoid - large transactions increase conflict chance
Transactions vs Batch Operations
| Feature | Transactions | Batch Operations |
|---|---|---|
| Atomicity | All succeed or all fail | Partial success possible |
| Capacity Cost | 2x standard operations | 1x standard operations |
| Conditional Expressions | Supported | Not supported |
| Max Items | 100 items or 4MB | 25 writes / 100 reads |
| Use Case | ACID requirements | Independent bulk operations |
Choose Transactions When:
- Operations must succeed or fail together
- You need conditional expressions across items
- Data consistency is critical
Choose Batch Operations When:
- Operations are independent
- Partial success is acceptable
- Cost optimization is important
See Also
- CRUD Operations - Basic single-item operations
- Batch Operations - Efficient bulk read/write operations
- Source Documentation - Complete API reference with advanced scenarios