Transactions¶
Dynorm provides ACID transactions for operations that must succeed or fail together.
Overview¶
DynamoDB transactions support up to 100 items across multiple tables with all-or-nothing semantics.
Repository Transactions¶
Type-safe transactions for a single repository:
err := UserRepo.Transaction().
Save(user1).
Save(user2).
DeleteByID("01HQ3K...").
Execute()
if err != nil {
// All operations rolled back
return err
}
// All operations committed
Available Operations¶
tx := UserRepo.Transaction()
// Save entity
tx.Save(user)
// Delete entity
tx.Delete(user)
// Delete by ID
tx.DeleteByID("01HQ3K...")
// Condition check (verify without modifying)
tx.ConditionCheck(id, expression.Name("Status").Equal(expression.Value("active")))
// Set idempotency key (prevents duplicate processing)
tx.WithIdempotencyKey("unique-request-id")
// Execute
err := tx.Execute()
Cross-Table Transactions¶
For transactions spanning multiple tables, use the low-level builder:
tx := transaction().
Put("users", userItem).
Put("orders", orderItem).
Delete("temp_data", tempKey).
WithIdempotencyKey("order-123")
err := tx.Execute()
Operations¶
// Put item
tx.Put(tableName, item)
// Put with condition
tx.PutWithCondition(tableName, item, condition)
// Update item
tx.Update(tableName, key, updateBuilder)
// Update with condition
tx.UpdateWithCondition(tableName, key, updateBuilder, condition)
// Delete item
tx.Delete(tableName, key)
// Delete with condition
tx.DeleteWithCondition(tableName, key, condition)
// Condition check only
tx.ConditionCheck(tableName, key, condition)
Read Transactions¶
Perform consistent reads across multiple items:
tx := transaction().
Get("users", userKey).
Get("orders", orderKey).
GetWithProjection("products", productKey, projection)
results, err := tx.ExecuteGet()
if err != nil {
return err
}
// results[0] = user item
// results[1] = order item
// results[2] = product item (projected fields only)
Conditional Writes¶
Add conditions to ensure data consistency:
import "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
// Only save if status is pending
condition := expression.Name("Status").Equal(expression.Value("pending"))
tx := UserRepo.Transaction().
ConditionCheck(user.ID, condition).
Save(user)
err := tx.Execute()
Idempotency¶
Prevent duplicate transaction processing:
tx := UserRepo.Transaction().
Save(order).
WithIdempotencyKey(order.ID) // Use order ID as idempotency key
err := tx.Execute()
// If called again with same key within 10 minutes,
// DynamoDB returns success without re-executing
Idempotency Window
DynamoDB maintains idempotency for 10 minutes after a successful transaction.
Examples¶
Create Order with Inventory Update¶
func CreateOrder(order *Order, productID string, quantity int) error {
// Build product key
productKey := map[string]types.AttributeValue{
"id": &types.AttributeValueMemberS{Value: productID},
}
// Stock decrement condition
stockCondition := expression.Name("Stock").GreaterThanEqual(
expression.Value(quantity),
)
// Stock update
stockUpdate := expression.Set(
expression.Name("Stock"),
expression.Name("Stock").Minus(expression.Value(quantity)),
)
// Transaction
tx := transaction().
Save(order, OrderRepo.GetTableName()).
UpdateWithCondition(
ProductRepo.GetTableName(),
productKey,
stockUpdate,
stockCondition,
).
WithIdempotencyKey(order.ID)
return tx.Execute()
}
Transfer Between Accounts¶
func Transfer(fromID, toID string, amount float64) error {
fromKey := map[string]types.AttributeValue{
"id": &types.AttributeValueMemberS{Value: fromID},
}
toKey := map[string]types.AttributeValue{
"id": &types.AttributeValueMemberS{Value: toID},
}
// Ensure sufficient balance
balanceCondition := expression.Name("Balance").GreaterThanEqual(
expression.Value(amount),
)
// Debit from source
debit := expression.Set(
expression.Name("Balance"),
expression.Name("Balance").Minus(expression.Value(amount)),
)
// Credit to destination
credit := expression.Set(
expression.Name("Balance"),
expression.Name("Balance").Plus(expression.Value(amount)),
)
tableName := AccountRepo.GetTableName()
return transaction().
UpdateWithCondition(tableName, fromKey, debit, balanceCondition).
Update(tableName, toKey, credit).
WithIdempotencyKey(fmt.Sprintf("transfer-%s-%s-%f", fromID, toID, amount)).
Execute()
}
Atomic Counter with Audit Log¶
func IncrementWithAudit(counterID string, delta int, reason string) error {
// Counter update
counterKey := map[string]types.AttributeValue{
"id": &types.AttributeValueMemberS{Value: counterID},
}
counterUpdate := expression.Set(
expression.Name("Value"),
expression.Name("Value").Plus(expression.Value(delta)),
)
// Audit log entry
auditEntry := &AuditLog{
CounterID: counterID,
Delta: delta,
Reason: reason,
Timestamp: time.Now(),
}
return transaction().
Update(CounterRepo.GetTableName(), counterKey, counterUpdate).
Save(auditEntry, AuditLogRepo.GetTableName()).
Execute()
}
Error Handling¶
err := tx.Execute()
if err != nil {
// Check error type
var txCanceled *types.TransactionCanceledException
if errors.As(err, &txCanceled) {
// Transaction was canceled
for i, reason := range txCanceled.CancellationReasons {
if reason.Code != nil {
fmt.Printf("Item %d failed: %s\n", i, *reason.Code)
}
}
}
return err
}
Common Errors¶
| Error | Cause |
|---|---|
TransactionCanceledException | Condition failed or conflict |
TransactionConflictException | Concurrent transaction on same item |
ValidationException | Invalid transaction structure |
IdempotentParameterMismatchException | Same idempotency key, different params |
Best Practices¶
Do
- Use transactions for operations that must be atomic
- Always use idempotency keys for important transactions
- Keep transactions small (fewer items = faster)
- Add conditions to detect conflicts
Don't
- Use transactions for single-item operations
- Exceed 100 items per transaction
- Reuse idempotency keys for different operations
- Ignore transaction cancellation reasons
Transaction Flow¶
sequenceDiagram
participant App
participant Dynorm
participant DynamoDB
App->>Dynorm: Transaction().Save(a).Save(b).Execute()
Dynorm->>Dynorm: Build TransactWriteItems
Dynorm->>DynamoDB: TransactWriteItems
alt All Succeed
DynamoDB-->>Dynorm: Success
Dynorm-->>App: nil (success)
else Any Fails
DynamoDB-->>Dynorm: TransactionCanceledException
Dynorm-->>App: Error with CancellationReasons
end Next Steps¶
- Batch Operations - Non-transactional bulk operations
- Relationships - Entity relationships
- Repository - Basic CRUD