Transactions in TerraScale: When and How to Use Them
Transactions are one of TerraScale’s most powerful features - and one of the most misunderstood. Let me clear up when you need them, when you don’t, and how to use them effectively.
What Are Transactions?
Section titled “What Are Transactions?”A transaction lets you perform multiple operations that either all succeed or all fail. There’s no middle ground.
await client.TransactWriteAsync(new[]{ new TransactWriteItem { Action = TransactAction.Update, PartitionKey = "account#sender", SortKey = "balance", UpdateExpression = "SET balance = balance - 100" }, new TransactWriteItem { Action = TransactAction.Update, PartitionKey = "account#receiver", SortKey = "balance", UpdateExpression = "SET balance = balance + 100" }});If either update fails, neither happens. The money doesn’t disappear into the void.
When You Need Transactions
Section titled “When You Need Transactions”Financial Operations
Section titled “Financial Operations”Moving money between accounts is the classic example. You need both the debit and credit to happen, or neither.
Inventory Management
Section titled “Inventory Management”Decrementing stock and creating an order should be atomic:
await client.TransactWriteAsync(new[]{ new TransactWriteItem { Action = TransactAction.Update, PartitionKey = "product#123", SortKey = "inventory", UpdateExpression = "SET quantity = quantity - 1", ConditionExpression = "quantity > 0" }, new TransactWriteItem { Action = TransactAction.Put, PartitionKey = "order#456", SortKey = "details", Data = orderData }});Cross-Entity Consistency
Section titled “Cross-Entity Consistency”When two entities must stay in sync:
// User joins a team - update both user and teamawait client.TransactWriteAsync(new[]{ new TransactWriteItem { Action = TransactAction.Update, PartitionKey = "user#123", SortKey = "profile", UpdateExpression = "SET teamId = :teamId", ExpressionAttributeValues = new { teamId = "team#456" } }, new TransactWriteItem { Action = TransactAction.Update, PartitionKey = "team#456", SortKey = "metadata", UpdateExpression = "SET memberCount = memberCount + 1" }});When You DON’T Need Transactions
Section titled “When You DON’T Need Transactions”Independent Operations
Section titled “Independent Operations”If operations don’t depend on each other, use batch writes instead:
// These are independent - use BatchWrite, not Transactionawait client.BatchWriteAsync(new[]{ new BatchWriteItem { Operation = BatchOperation.Put, ... }, new BatchWriteItem { Operation = BatchOperation.Put, ... }, new BatchWriteItem { Operation = BatchOperation.Put, ... }});BatchWrite is faster and cheaper than TransactWrite.
Single Item Updates
Section titled “Single Item Updates”For a single item, a regular PutItem or UpdateItem with a condition expression is sufficient:
await client.UpdateItemAsync("account#123", "balance", "SET balance = balance - 100", conditionExpression: "balance >= 100");Read-Only Operations
Section titled “Read-Only Operations”For reading multiple items, use BatchGet or parallel queries:
var items = await client.BatchGetAsync(keys);TransactGet exists for cases where you need a consistent snapshot across items, but it’s rarely necessary.
Transaction Limits
Section titled “Transaction Limits”Know the limits:
| Limit | Value |
|---|---|
| Items per transaction | 25 |
| Total size | 4MB |
| Items per partition | No limit |
| Cross-region | Not supported |
If you need more than 25 items, you’ll need to restructure your operation or accept eventual consistency for some parts.
Condition Expressions
Section titled “Condition Expressions”The real power of transactions comes from condition expressions:
new TransactWriteItem{ Action = TransactAction.Update, PartitionKey = "product#123", SortKey = "inventory", UpdateExpression = "SET quantity = quantity - :amount", ConditionExpression = "quantity >= :amount", ExpressionAttributeValues = new { amount = 5 }}If the condition fails, the entire transaction is cancelled. This prevents race conditions.
Handling Transaction Failures
Section titled “Handling Transaction Failures”Transactions can fail for several reasons:
var result = await client.TransactWriteAsync(items);
if (result.IsFailed){ var error = result.Errors.First();
if (error.Message.Contains("ConditionalCheckFailed")) { // A condition wasn't met - maybe retry with fresh data } else if (error.Message.Contains("TransactionCanceled")) { // Conflict with another transaction - retry } else { // Something else went wrong }}Idempotency Tokens
Section titled “Idempotency Tokens”For critical transactions, use idempotency tokens to prevent duplicate execution:
var result = await client.TransactWriteAsync( items, clientRequestToken: $"order-{orderId}-payment");If the same token is used within 10 minutes, TerraScale returns the previous result without re-executing.
Performance Considerations
Section titled “Performance Considerations”Transactions are more expensive than regular operations:
- ~2x the latency of a single write
- ~2x the cost in write units
- Lock contention on hot items
Use them when you need atomicity, not as a default.
A Real Example
Section titled “A Real Example”Here’s a complete example of a purchase transaction:
public async Task<Result> ProcessPurchase(string userId, string productId, int quantity){ var result = await _db.TransactWriteAsync(new[] { // Decrement inventory new TransactWriteItem { Action = TransactAction.Update, PartitionKey = $"product#{productId}", SortKey = "inventory", UpdateExpression = "SET quantity = quantity - :qty", ConditionExpression = "quantity >= :qty", ExpressionAttributeValues = new { qty = quantity } }, // Create order new TransactWriteItem { Action = TransactAction.Put, PartitionKey = $"user#{userId}", SortKey = $"order#{DateTime.UtcNow:O}", Data = new Dictionary<string, object> { ["productId"] = productId, ["quantity"] = quantity, ["status"] = "pending" } }, // Update user's order count new TransactWriteItem { Action = TransactAction.Update, PartitionKey = $"user#{userId}", SortKey = "stats", UpdateExpression = "SET orderCount = if_not_exists(orderCount, 0) + 1" } });
return result;}All three operations succeed together, or none of them do.
Summary
Section titled “Summary”- Use transactions when operations must be atomic
- Use batch operations when operations are independent
- Keep transactions small (under 25 items)
- Use condition expressions to prevent race conditions
- Handle failures gracefully with retries
Questions? Reach out at mariogk@terrascale.tech.