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