When to Use DynamoDB Transactions: ACID in a NoSQL Store
Need all‑or‑nothing writes across DynamoDB tables? DynamoDB transactions let you atomically write up to 100 items, but double the WCU cost and hit limits. This guide shows how to decide, code a sample, and watch for pitfalls.
20 Jul 2025, 04:25 UTC

Problem: Partial Writes in a Distributed System
In many applications you need to write several items that belong to the same logical operation—think reserving inventory, placing an order, or updating a user profile that spans multiple tables. If one write succeeds and another fails, the system ends up in an inconsistent state, and you must write custom compensation logic to undo the partial changes.
Thesis: DynamoDB Transactions as a Built‑in Saga
DynamoDB’s TransactWriteItems and TransactGetItems provide true ACID semantics for up to 100 items in a single AWS region. They eliminate the need for application‑level rollbacks, but they come with higher cost, latency, and size limits. Choosing a transaction is a trade‑off: you get consistency for the price of extra capacity and a stricter size envelope.
Section 1 – How a Transaction Works
A transaction is a two‑phase commit. During the prepare phase, DynamoDB checks all condition expressions, locks the items, and reserves the necessary write capacity. If everything passes, it moves to the commit phase, where the changes are applied atomically. The client never sees the intermediate state.
Key points:
- All-or-nothing: either every write succeeds or none are applied.
- Snapshot isolation for reads:
TransactGetItemsreturns a view as of the transaction start time. - Condition expressions (e.g.
attribute_not_exists,version = :v) are evaluated atomically across all items.
Cost Implication
Each write in a transaction consumes twice the WCUs of a normal PutItem or UpdateItem. On-demand tables pay double the on-demand price, and provisioned tables need headroom for the extra capacity. For example, a single PutItem that would normally use 1 WCU now consumes 2 WCUs during the transaction.
| Operation | Regular WCU | Transaction WCU |
|---|---|---|
| PutItem | 1 | 2 |
| UpdateItem | 1 | 2 |
| DeleteItem | 1 | 2 |
Monitor ConsumedWriteCapacityUnits in CloudWatch to spot spikes.
Section 2 – A Concrete Example
Below is a minimal, reproducible example that writes three items across two tables atomically, uses optimistic locking, and demonstrates idempotency.
{
"TransactItems": [
{
"Put": {
"TableName": "Orders",
"Item": {
"OrderId": {"S": "order123"},
"UserId": {"S": "user456"},
"Status": {"S": "PENDING"},
"Version": {"N": "1"}
},
"ConditionExpression": "attribute_not_exists(OrderId)"
}
},
{
"Update": {
"TableName": "Inventory",
"Key": {"Sku": {"S": "ABC123"}},
"UpdateExpression": "SET Qty = Qty - :qty",
"ConditionExpression": "Qty >= :qty",
"ExpressionAttributeValues": {
":qty": {"N": "2"}
}
}
},
{
"Put": {
"TableName": "OrderItems",
"Item": {
"OrderId": {"S": "order123"},
"ItemIndex": {"N": "1"},
"Sku": {"S": "ABC123"},
"Qty": {"N": "2"}
},
"ConditionExpression": "attribute_not_exists(OrderId) AND attribute_not_exists(ItemIndex)"
}
}
],
"ClientRequestToken": "unique-token-xyz"
}
Run it with the AWS CLI:
aws dynamodb transact-write-items \
--transact-items file://tx.json \
--client-request-token unique-token-xyz
The first call writes the new order, reserves inventory, and creates the order item. If any condition fails, the entire operation is aborted and you receive TransactionCanceledException with details in CancellationReasons.
Retry the same JSON with the same ClientRequestToken within 10 minutes. DynamoDB will return the original result without re‑executing the writes, giving you exactly‑once semantics.
Section 3 – Trade‑offs & Limitations
- 100‑Item Ceiling: If your workflow needs more than 100 items, you must split it into multiple transactions and orchestrate with Step Functions or a saga pattern.
- 4 MB Payload: The entire request, including items and condition expressions, must stay below 4 MB. Large items or many conditions can hit this limit even before the 100‑item cap.
- No Global Tables: Transactions cannot span tables that are part of a Global Table; you’ll need per‑region conditional writes or a saga.
- Higher Latency: Two‑phase commit adds 2–5 ms on top of a single write, which may be noticeable in latency‑sensitive paths.
- Non‑Idempotent by Default: Without a
ClientRequestToken, retrying a failed transaction can re‑apply writes. Use the token for exactly‑once semantics. - Read Isolation:
TransactGetItemsgives a snapshot view; concurrent non‑transactional writes can still occur after the snapshot, so plan for write skew if your invariants cross transactional boundaries.
Section 4 – How to Verify Your Transaction Works
After executing the transaction, confirm the results by querying the tables:
aws dynamodb get-item \
--table-name Orders \
--key '{"OrderId": {"S": "order123"}}'
Check CloudWatch for ConsumedWriteCapacityUnits to ensure you’re seeing the expected 2× WCU per item. If you notice unexpected spikes, revisit your transaction size or consider batching.
Conclusion – When to Pick a Transaction
Use DynamoDB transactions when:
- You need atomicity across multiple items or tables.
- Condition expressions span items and you want optimistic locking.
- You can tolerate the doubled WCU cost and the 100‑item/4 MB limits.
Otherwise, for simple single‑item writes or when throughput is critical, stick with standard PutItem or UpdateItem and handle compensation logic yourself.
Next steps: instrument CloudWatch metrics, add ClientRequestToken for idempotency, and consider Step Functions if you hit the 100‑item ceiling. That will give you a robust, ACID‑compliant workflow without sacrificing scalability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.