DynamoDB TransactWrite: All-or-Nothing Writes Explained
DynamoDB is great at fast single-item reads and writes. The hard part shows up when one business action needs multiple items to change together.
Examples:
- Decrease stock and create an order
- Debit one wallet and credit another
- Book a seat and mark it unavailable
If you do those as separate UpdateItem / PutItem calls, you can end up with partial updates. That’s where TransactWriteItems helps.
The problem in one sentence
Separate writes can leave your data in a half-finished state.
Example: e-commerce order
Without a transaction:
1. Update inventory: stock 10 → 9 ✅
2. Create order record ❌ fails
Result: stock went down, but no order exists. Support tickets follow.
You can add rollback code. It helps, but rollback is not guaranteed (timeouts, crashes, partial failures).
Better approach: treat both writes as one atomic operation.
What is TransactWriteItems?
TransactWriteItems lets you send multiple write actions in one request:
PutUpdateDeleteConditionCheck
DynamoDB applies them as a single transaction:
- ✅ All conditions pass → all writes succeed
- ❌ Any condition fails → no writes are applied
Up to 25 actions per transaction (across up to 10 tables).
Practical example 1: Place an order + reduce inventory
Data model
| PK | SK | Purpose |
|---|---|---|
PRODUCT#sku-123 |
META |
Stock count |
USER#42 |
ORDER#2026-001 |
Order record |
Transaction intent
- Reduce
stockonly if enough is available - Create order only if stock update succeeds
await dynamo.transactWriteItems({
transactItems: [
{
update: {
key: { pk: 'PRODUCT#sku-123', sk: 'META' },
updateExpression: 'SET stock = stock - :qty, updatedAt = :now',
conditionExpression: 'stock >= :qty',
expressionAttributeValues: {
':qty': 1,
':now': Date.now(),
},
},
},
{
put: {
item: {
pk: 'USER#42',
sk: 'ORDER#2026-001',
productId: 'sku-123',
quantity: 1,
status: 'CREATED',
},
conditionExpression: 'attribute_not_exists(pk)',
},
},
],
});
If stock is insufficient, the whole transaction fails. No order, no inventory change.
Practical example 2: Wallet transfer (debit + credit)
Moving $50 from User A to User B must be atomic.
| PK | SK | Fields |
|---|---|---|
ACCOUNT#A |
BALANCE |
amount |
ACCOUNT#B |
BALANCE |
amount |
ACCOUNT#A |
TXN#uuid |
transfer log |
await dynamo.transactWriteItems({
transactItems: [
{
update: {
key: { pk: 'ACCOUNT#A', sk: 'BALANCE' },
updateExpression: 'SET amount = amount - :value',
conditionExpression: 'amount >= :value',
expressionAttributeValues: { ':value': 50 },
},
},
{
update: {
key: { pk: 'ACCOUNT#B', sk: 'BALANCE' },
updateExpression: 'SET amount = amount + :value',
expressionAttributeValues: { ':value': 50 },
},
},
{
put: {
item: {
pk: 'ACCOUNT#A',
sk: 'TXN#uuid',
type: 'TRANSFER',
to: 'ACCOUNT#B',
value: 50,
},
},
},
],
});
Without a transaction, you risk:
- A debited with no credit to B
- Or duplicate transfer records on retries
Practical example 3: Seat booking
Conference seat A12 must not be sold twice.
| PK | SK | Meaning |
|---|---|---|
EVENT#conf-2026 |
SEAT#A12 |
Seat status |
USER#99 |
BOOKING#A12 |
User booking |
await dynamo.transactWriteItems({
transactItems: [
{
update: {
key: { pk: 'EVENT#conf-2026', sk: 'SEAT#A12' },
updateExpression: 'SET #status = :booked, bookedBy = :userId',
conditionExpression: '#status = :available',
expressionAttributeNames: { '#status': 'status' },
expressionAttributeValues: {
':available': 'AVAILABLE',
':booked': 'BOOKED',
':userId': 99,
},
},
},
{
put: {
item: {
pk: 'USER#99',
sk: 'BOOKING#A12',
eventId: 'conf-2026',
seat: 'A12',
},
conditionExpression: 'attribute_not_exists(pk)',
},
},
],
});
Two users booking the same seat at the same time? One transaction wins, one fails safely.
Optimistic locking (common in production)
Sometimes you read a value, compute the next value in code, then write with a guard:
- Read
version = 7 - Update only if
versionis still7 - Set
version = 8
conditionExpression: #version = :expectedVersion
If another process updated first, your transaction is rejected. No silent overwrite.
This pattern is widely used for inventory, quotas, and counters.
ConditionExpression vs UpdateExpression (important)
A common production mistake: using update-style logic inside conditions.
| Feature | UpdateExpression | ConditionExpression |
|---|---|---|
if_not_exists() |
✅ | ❌ |
Arithmetic (a + b) |
✅ | ❌ (very limited) |
Comparisons (=, <, >=) |
— | ✅ |
attribute_exists() |
— | ✅ |
What fails
if_not_exists(stock, :zero) + :qty <= maxStock
❌ Arithmetic / if_not_exists in conditions often fails.
Better pattern
// App-level validation
if (currentStock < qty) throw new Error('Insufficient stock');
// Condition: simple guard
conditionExpression: 'stock >= :qty';
Rule of thumb
- UpdateExpression → compute new state
- ConditionExpression → simple safety checks
When to use TransactWrite
Good fit ✅
- Multi-item business invariants (order + inventory)
- Financial movements (debit + credit)
- Booking systems (resource + reservation)
- Parent/child record creation that must be together
Usually not needed ❌
- Single-item updates
- Read-heavy workflows
- Very hot partition keys under extreme contention
- Long-running processes (use saga/workflow orchestration instead)
Limits to remember
| Limit | Value |
|---|---|
| Actions per transaction | 25 |
| Tables per transaction | 10 |
| Same item twice in one transaction | ❌ |
| Cross-region transaction | ❌ |
Handling failures
If a condition fails, DynamoDB returns TransactionCanceledException.
Typical handling:
- Re-read latest state
- Retry 2–3 times (for contention)
- Return a clear business error (“Out of stock”, “Seat already taken”)
Also ensure every value in ExpressionAttributeValues is actually used in the expression—unused values cause request validation errors.
Optional: temporary holds with TTL
For bookings/reservations, add expiry:
{
"status": "HELD",
"expiresAt": 1735689600,
"ttl": 1735689600
}
expiresAtfor app logicttlfor DynamoDB automatic cleanup
Enable TTL once:
aws dynamodb update-time-to-live \
--table-name YOUR_TABLE \
--time-to-live-specification "Enabled=true, AttributeName=ttl"
Important: TTL deletion alone may not fix derived counters. Add cleanup logic (or Streams) if counters must be adjusted when holds expire.
Flow diagram
Client request
│
▼
TransactWrite (all required items)
│
├─ Success → business action complete
│
└─ Fail → no partial writes, safe retry/error response
Takeaway
DynamoDB won’t automatically keep multi-item workflows consistent.TransactWriteItems is the built-in tool for:
“These writes represent one business action—never allow half success.”
Use it for orders, transfers, bookings, and any workflow where partial updates are unacceptable.