Skip to content

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.

const MaxTransactionItems = 100

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