Skip to main content

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​

FeatureTransactionsBatch Operations
AtomicityAll succeed or all failPartial success possible
Capacity Cost2x standard operations1x standard operations
Conditional ExpressionsSupportedNot supported
Max Items100 items or 4MB25 writes / 100 reads
Use CaseACID requirementsIndependent 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​