Building DynamoDB Transactions: Coordinating Atomic, Multi-Item Writes

 

Building DynamoDB Transactions: Coordinating Atomic, Multi-Item Writes

Learn how to perform atomic, multi-item operations in DynamoDB using transactions.





The Build

In this tutorial, you’ll build an Orders and Payments workflow where both records succeed or fail together. You’ll use the AWS CLI to create two tables, perform a transactional write, verify atomicity, and then clean up your environment.

Why It Matters

Most DynamoDB writes affect a single item at a time. But real-world systems — like checkout or billing — often need to update multiple items that depend on each other. That’s where transactions come in. They let you group several operations into a single all-or-nothing block, ensuring your data stays consistent even under failure or concurrency.

What Is Atomicity?

In database terms, atomicity means that either all parts of a transaction succeed or none do. If one write fails, DynamoDB automatically rolls back the others, guaranteeing that your data never ends up half-complete. It’s the “safety lock” for multi-item operations.


Step 1 – Create the Orders and Payments Tables

You’ll create two tables:

  • Orders (keyed by OrderID)
  • Payments (keyed by PaymentID)

Both will use PAY_PER_REQUEST billing for simplicity.

aws dynamodb create-table \
  --table-name Orders \
  --attribute-definitions AttributeName=OrderID,AttributeType=S \
  --key-schema AttributeName=OrderID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

aws dynamodb create-table \
  --table-name Payments \
  --attribute-definitions AttributeName=PaymentID,AttributeType=S \
  --key-schema AttributeName=PaymentID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST

Output (example):

{
  "TableDescription": {
    "TableName": "Orders",
    "TableStatus": "CREATING"
  }
}

Check table status until both read ACTIVE:

aws dynamodb describe-table --table-name Orders --query "Table.TableStatus"
aws dynamodb describe-table --table-name Payments --query "Table.TableStatus"

Output:

"ACTIVE"

Step 2 – Perform a Transactional Write

Now you’ll insert an order and its corresponding payment in a single atomic operation using transact-write-items.

aws dynamodb transact-write-items \
  --transact-items '[
    {
      "Put": {
        "TableName": "Orders",
        "Item": {
          "OrderID": {"S": "ORD#1001"},
          "Customer": {"S": "alice"},
          "Amount": {"N": "59.99"},
          "Status": {"S": "PENDING"}
        }
      }
    },
    {
      "Put": {
        "TableName": "Payments",
        "Item": {
          "PaymentID": {"S": "PAY#9001"},
          "OrderID": {"S": "ORD#1001"},
          "Method": {"S": "CreditCard"},
          "Amount": {"N": "59.99"}
        }
      }
    }
  ]'

Output:

{}

An empty response means both records were committed successfully — all or nothing.


Step 3 – Verify the Data Across Tables

Confirm that both items exist:

aws dynamodb get-item --table-name Orders --key '{"OrderID":{"S":"ORD#1001"}}'
aws dynamodb get-item --table-name Payments --key '{"PaymentID":{"S":"PAY#9001"}}'

Output (Orders):

{
  "Item": {
    "OrderID": {"S": "ORD#1001"},
    "Customer": {"S": "alice"},
    "Amount": {"N": "59.99"},
    "Status": {"S": "PENDING"}
  }
}

Output (Payments):

{
  "Item": {
    "PaymentID": {"S": "PAY#9001"},
    "OrderID": {"S": "ORD#1001"},
    "Amount": {"N": "59.99"},
    "Method": {"S": "CreditCard"}
  }
}

Step 4 – Test Transaction Failure

Now try a transaction that violates a condition — DynamoDB should roll back both writes.
Here, we’ll prevent the same OrderID from being reused:

aws dynamodb transact-write-items \
  --transact-items '[
    {
      "Put": {
        "TableName": "Orders",
        "Item": {
          "OrderID": {"S": "ORD#1001"},
          "Customer": {"S": "bob"},
          "Amount": {"N": "75.00"},
          "Status": {"S": "PENDING"}
        },
        "ConditionExpression": "attribute_not_exists(OrderID)"
      }
    },
    {
      "Put": {
        "TableName": "Payments",
        "Item": {
          "PaymentID": {"S": "PAY#9002"},
          "OrderID": {"S": "ORD#1001"},
          "Method": {"S": "CreditCard"},
          "Amount": {"N": "75.00"}
        }
      }
    }
  ]'

Expected Output (Error):

{
  "__type": "TransactionCanceledException",
  "CancellationReasons": [
    {"Code": "ConditionalCheckFailed", "Message": "OrderID already exists"}
  ]
}

In this case, the Orders table triggered the failure because the OrderID already existed — proving that DynamoDB aborted the entire transaction to maintain consistency.

No data was written — both tables rolled back atomically.


Step 5 – Update Order and Payment Atomically

Now you’ll update both tables in one transaction — marking the order as PAID and the payment as PROCESSED.

aws dynamodb transact-write-items \
  --transact-items '[
    {
      "Update": {
        "TableName": "Orders",
        "Key": {"OrderID": {"S": "ORD#1001"}},
        "UpdateExpression": "SET #S = :s",
        "ExpressionAttributeNames": {"#S": "Status"},
        "ExpressionAttributeValues": {":s": {"S": "PAID"}}
      }
    },
    {
      "Update": {
        "TableName": "Payments",
        "Key": {"PaymentID": {"S": "PAY#9001"}},
        "UpdateExpression": "SET #S = :s",
        "ExpressionAttributeNames": {"#S": "Status"},
        "ExpressionAttributeValues": {":s": {"S": "PROCESSED"}}
      }
    }
  ]'

Output:

{}

Check your updates:

aws dynamodb get-item --table-name Orders --key '{"OrderID":{"S":"ORD#1001"}}' --projection-expression "OrderID, Status"
aws dynamodb get-item --table-name Payments --key '{"PaymentID":{"S":"PAY#9001"}}' --projection-expression "PaymentID, Status"

Output (Orders):

{"Item": {"OrderID": {"S": "ORD#1001"}, "Status": {"S": "PAID"}}}

Output (Payments):

{"Item": {"PaymentID": {"S": "PAY#9001"}, "Status": {"S": "PROCESSED"}}}

Step 6 – Clean Up

To wrap up this exercise, you’ll remove all the resources you created — deleting both tables to keep your AWS environment tidy and cost-free.

Run the following commands to delete the Orders and Payments tables:

aws dynamodb delete-table --table-name Orders
aws dynamodb delete-table --table-name Payments

After a short delay, confirm that the tables were successfully deleted:

aws dynamodb list-tables

Output:

{"TableNames": []}

An empty list means both tables have been deleted successfully.


Wrap-Up

You’ve now built a fully atomic, multi-item workflow in DynamoDB.

Transactions ensure data consistency across multiple items or tables — a powerful feature for any distributed system design.


Pro Tip #1 — Keep Transactions Small

Transactions can include up to 25 items or 4 MB total data.

Keep payloads minimal for reliability and cost efficiency.

Transactions introduce a small amount of additional latency and cost compared to single-item writes, so reserve them for cases where data integrity outweighs performance sensitivity.


Pro Tip #2 — Combine with Condition Expressions

You can mix conditions inside a transaction to enforce complex business logic (like ensuring the payment only applies to pending orders).


Pro Tip #3 — Consistent Multi-Item Reads

DynamoDB also supports TransactGetItems, which lets you read up to 25 items across tables in a single atomic read operation. It guarantees that all items are returned from the same consistent point in time — ideal for validation steps before committing a transaction.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Comments

Popular posts from this blog

The New ChatGPT Reason Feature: What It Is and Why You Should Use It

Raspberry Pi Connect vs. RealVNC: A Comprehensive Comparison

Insight: The Great Minimal OS Showdown—DietPi vs Raspberry Pi OS Lite