Transactions¶
This guide covers DynamoDB transactions, which provide ACID (Atomicity, Consistency, Isolation, Durability) guarantees for multi-item operations. Transactions ensure that all operations succeed or all fail together.
Overview¶
DynamoDB provides two transaction operations:
- TransactWrite - Atomically write up to 100 items (put, update, delete, conditionCheck)
- TransactGet - Atomically read up to 100 items at the same point in time
ACID Properties: - Atomicity - All operations succeed or all fail - Consistency - Data remains in a valid state - Isolation - Concurrent transactions don't interfere - Durability - Committed changes are permanent
Transactwrite operation¶
Execute multiple write operations atomically.
Basic transactwrite¶
import { TableClient } from '@ddb-lib/client'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
const client = new TableClient({
tableName: 'my-table',
client: new DynamoDBClient({ region: 'us-east-1' })
})
// All operations succeed or all fail
await client.transactWrite([
{
type: 'put',
item: {
pk: 'USER#123',
sk: 'PROFILE',
name: 'Alice',
email: 'alice@example.com'
}
},
{
type: 'update',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
updates: {
balance: 1000,
updatedAt: new Date().toISOString()
}
},
{
type: 'delete',
key: { pk: 'TEMP#123', sk: 'DATA' }
}
])
console.log('Transaction completed successfully')
Transaction flow¶
sequenceDiagram
participant App as Application
participant Client as TableClient
participant DDB as DynamoDB
App->>Client: transactWrite([ops])
Client->>DDB: TransactWriteItems
alt All Conditions Pass
DDB->>DDB: Execute All Operations
DDB-->>Client: Success
Client-->>App: void
else Any Condition Fails
DDB->>DDB: Rollback All
DDB-->>Client: TransactionCanceledException
Client-->>App: Error
end Put operation¶
Create or replace items within a transaction:
await client.transactWrite([
{
type: 'put',
item: {
pk: 'ORDER#456',
sk: 'DETAILS',
orderId: '456',
userId: '123',
total: 99.99,
status: 'PENDING',
createdAt: new Date().toISOString()
}
},
{
type: 'put',
item: {
pk: 'USER#123',
sk: 'ORDER#456',
orderId: '456',
total: 99.99,
createdAt: new Date().toISOString()
}
}
])
Update operation¶
Modify existing items:
await client.transactWrite([
{
type: 'update',
key: { pk: 'ORDER#456', sk: 'DETAILS' },
updates: {
status: 'PROCESSING',
processedAt: new Date().toISOString()
}
},
{
type: 'update',
key: { pk: 'INVENTORY#789', sk: 'STOCK' },
updates: {
quantity: 95, // Decrement by 5
lastUpdated: new Date().toISOString()
}
}
])
Delete operation¶
Remove items atomically:
await client.transactWrite([
{
type: 'delete',
key: { pk: 'SESSION#abc123', sk: 'DATA' }
},
{
type: 'delete',
key: { pk: 'CACHE#abc123', sk: 'ENTRY' }
}
])
Condition check¶
Verify conditions without modifying data:
await client.transactWrite([
// Check that account has sufficient balance
{
type: 'conditionCheck',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
condition: {
balance: { gte: 100 }
}
},
// If check passes, deduct from balance
{
type: 'update',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
updates: {
balance: 900, // Deduct 100
lastTransaction: new Date().toISOString()
}
},
// And create transaction record
{
type: 'put',
item: {
pk: 'TRANSACTION#tx-789',
sk: 'DETAILS',
accountId: '123',
amount: -100,
type: 'DEBIT',
timestamp: new Date().toISOString()
}
}
])
Conditional operations¶
Add conditions to any operation:
await client.transactWrite([
{
type: 'put',
item: {
pk: 'USER#123',
sk: 'PROFILE',
name: 'Alice',
version: 1
},
// Only create if doesn't exist
condition: {
pk: { attributeNotExists: true }
}
},
{
type: 'update',
key: { pk: 'COUNTER#views', sk: 'PAGE#home' },
updates: {
count: 1001
},
// Only update if current count is 1000
condition: {
count: { eq: 1000 }
}
},
{
type: 'delete',
key: { pk: 'TEMP#data', sk: 'OLD' },
// Only delete if expired
condition: {
expiresAt: { lt: Date.now() }
}
}
])
Transactget operation¶
Read multiple items atomically at the same point in time.
Basic transactget¶
// Read multiple items with snapshot isolation
const items = await client.transactGet([
{ pk: 'USER#123', sk: 'PROFILE' },
{ pk: 'ACCOUNT#123', sk: 'BALANCE' },
{ pk: 'ORDER#456', sk: 'DETAILS' }
])
// All items are from the same point in time
const [user, account, order] = items
if (user) {
console.log('User:', user.name)
}
if (account) {
console.log('Balance:', account.balance)
}
if (order) {
console.log('Order:', order.orderId)
}
Transactget with projection¶
Retrieve only specific attributes:
const items = await client.transactGet(
[
{ pk: 'USER#123', sk: 'PROFILE' },
{ pk: 'USER#456', sk: 'PROFILE' },
{ pk: 'USER#789', sk: 'PROFILE' }
],
{
projectionExpression: ['name', 'email', 'status']
}
)
// Each item contains only: pk, sk, name, email, status
Handling missing items¶
TransactGet returns null for missing items:
const items = await client.transactGet([
{ pk: 'USER#123', sk: 'PROFILE' },
{ pk: 'USER#999', sk: 'PROFILE' }, // Doesn't exist
{ pk: 'USER#456', sk: 'PROFILE' }
])
// items = [{ ...user123 }, null, { ...user456 }]
items.forEach((item, index) => {
if (item) {
console.log(`Item ${index}:`, item)
} else {
console.log(`Item ${index}: Not found`)
}
})
Common use cases¶
1. money transfer¶
Atomically transfer funds between accounts:
const fromAccount = await client.get({ pk: 'ACCOUNT#123', sk: 'BALANCE' })
const toAccount = await client.get({ pk: 'ACCOUNT#456', sk: 'BALANCE' })
if (!fromAccount || !toAccount) {
throw new Error('Account not found')
}
const amount = 100
await client.transactWrite([
// Check sufficient balance
{
type: 'conditionCheck',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
condition: {
balance: { gte: amount }
}
},
// Deduct from source
{
type: 'update',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
updates: {
balance: fromAccount.balance - amount,
updatedAt: new Date().toISOString()
}
},
// Add to destination
{
type: 'update',
key: { pk: 'ACCOUNT#456', sk: 'BALANCE' },
updates: {
balance: toAccount.balance + amount,
updatedAt: new Date().toISOString()
}
},
// Create transaction record
{
type: 'put',
item: {
pk: 'TRANSACTION#tx-789',
sk: 'DETAILS',
from: '123',
to: '456',
amount,
timestamp: new Date().toISOString()
}
}
])
console.log('Transfer completed successfully')
2. inventory management¶
Atomically update inventory and create order:
const product = await client.get({ pk: 'PRODUCT#789', sk: 'INVENTORY' })
if (!product) {
throw new Error('Product not found')
}
const quantity = 5
await client.transactWrite([
// Check sufficient stock
{
type: 'conditionCheck',
key: { pk: 'PRODUCT#789', sk: 'INVENTORY' },
condition: {
stock: { gte: quantity }
}
},
// Decrement inventory
{
type: 'update',
key: { pk: 'PRODUCT#789', sk: 'INVENTORY' },
updates: {
stock: product.stock - quantity,
lastUpdated: new Date().toISOString()
}
},
// Create order
{
type: 'put',
item: {
pk: 'ORDER#456',
sk: 'DETAILS',
productId: '789',
quantity,
status: 'PENDING',
createdAt: new Date().toISOString()
}
},
// Add to user's orders
{
type: 'put',
item: {
pk: 'USER#123',
sk: 'ORDER#456',
orderId: '456',
productId: '789',
quantity,
createdAt: new Date().toISOString()
}
}
])
3. optimistic locking¶
Prevent lost updates with version checking:
import { PatternHelpers } from '@ddb-lib/core'
const item = await client.get({ pk: 'DOCUMENT#123', sk: 'CONTENT' })
if (!item) {
throw new Error('Document not found')
}
try {
await client.transactWrite([
{
type: 'update',
key: { pk: 'DOCUMENT#123', sk: 'CONTENT' },
updates: {
content: 'Updated content',
version: PatternHelpers.incrementVersion(item.version),
updatedAt: new Date().toISOString()
},
// Only update if version hasn't changed
condition: {
version: { eq: item.version }
}
}
])
console.log('Document updated successfully')
} catch (error) {
if (error.name === 'TransactionCanceledException') {
console.log('Version conflict - document was modified by another user')
// Retry the operation
}
}
4. idempotent operations¶
Use client request tokens for idempotency:
import { randomUUID } from 'crypto'
const requestToken = randomUUID()
// First attempt
await client.transactWrite(
[
{
type: 'put',
item: {
pk: 'PAYMENT#123',
sk: 'DETAILS',
amount: 99.99,
status: 'COMPLETED',
requestToken
}
}
],
{
clientRequestToken: requestToken
}
)
// Retry with same token (safe - won't duplicate)
await client.transactWrite(
[
{
type: 'put',
item: {
pk: 'PAYMENT#123',
sk: 'DETAILS',
amount: 99.99,
status: 'COMPLETED',
requestToken
}
}
],
{
clientRequestToken: requestToken // Same token = idempotent
}
)
5. multi-entity updates¶
Update related entities atomically:
const userId = '123'
const newEmail = 'newemail@example.com'
await client.transactWrite([
// Update user profile
{
type: 'update',
key: { pk: `USER#${userId}`, sk: 'PROFILE' },
updates: {
email: newEmail,
updatedAt: new Date().toISOString()
}
},
// Update user settings
{
type: 'update',
key: { pk: `USER#${userId}`, sk: 'SETTINGS' },
updates: {
notificationEmail: newEmail,
updatedAt: new Date().toISOString()
}
},
// Update email index
{
type: 'put',
item: {
pk: `EMAIL#${newEmail}`,
sk: 'USER',
userId,
createdAt: new Date().toISOString()
}
},
// Delete old email index
{
type: 'delete',
key: { pk: 'EMAIL#oldemail@example.com', sk: 'USER' }
}
])
6. consistent reads across items¶
Read related items at the same point in time:
// Get user, account, and recent orders atomically
const [user, account, ...orders] = await client.transactGet([
{ pk: 'USER#123', sk: 'PROFILE' },
{ pk: 'ACCOUNT#123', sk: 'BALANCE' },
{ pk: 'USER#123', sk: 'ORDER#001' },
{ pk: 'USER#123', sk: 'ORDER#002' },
{ pk: 'USER#123', sk: 'ORDER#003' }
])
// All data is from the same snapshot
if (user && account) {
console.log(`${user.name} has balance: ${account.balance}`)
console.log(`Recent orders: ${orders.filter(Boolean).length}`)
}
Transaction limits¶
DynamoDB limits¶
| Limit | Value |
|---|---|
| Max operations per transaction | 100 |
| Max request size | 4 MB |
| Max item size | 400 KB |
| Max tables per transaction | 1 |
| Idempotency window | 10 minutes |
Cost considerations¶
Transactions consume 2x the capacity units:
// Regular operations
await client.put(item) // 1 WCU
await client.get(key) // 1 RCU
// Transactional operations
await client.transactWrite([{ type: 'put', item }]) // 2 WCU
await client.transactGet([key]) // 2 RCU
When to use transactions: - ✅ When atomicity is required (financial operations, inventory) - ✅ When consistency across items is critical - ✅ When the 2x cost is acceptable - ❌ For simple single-item operations - ❌ When eventual consistency is acceptable
Error handling¶
Transaction cancellation¶
try {
await client.transactWrite([
{
type: 'update',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
updates: { balance: 900 },
condition: { balance: { gte: 100 } }
},
{
type: 'put',
item: { pk: 'TRANSACTION#tx-789', sk: 'DETAILS', amount: -100 }
}
])
} catch (error) {
if (error.name === 'TransactionCanceledException') {
console.log('Transaction failed - condition not met')
console.log('Cancellation reasons:', error.CancellationReasons)
// Check which operation failed
error.CancellationReasons?.forEach((reason, index) => {
if (reason.Code === 'ConditionalCheckFailed') {
console.log(`Operation ${index} condition failed`)
}
})
}
}
Common errors¶
try {
await client.transactWrite(operations)
} catch (error) {
if (error.name === 'TransactionCanceledException') {
// One or more conditions failed
console.error('Transaction cancelled:', error.message)
} else if (error.name === 'ValidationException') {
// Invalid request (too many items, wrong format, etc.)
console.error('Invalid transaction:', error.message)
} else if (error.name === 'ProvisionedThroughputExceededException') {
// Throttled - retry with backoff
console.error('Throttled - retry later')
} else if (error.name === 'TransactionInProgressException') {
// Duplicate request token with different operations
console.error('Transaction already in progress')
} else {
console.error('Transaction failed:', error)
}
}
Best practices¶
1. keep transactions small¶
// ❌ Bad: Large transaction with 100 operations
await client.transactWrite(Array(100).fill({...}))
// ✅ Good: Smaller transactions
await client.transactWrite([
// Only operations that must be atomic
{ type: 'update', ... },
{ type: 'put', ... },
{ type: 'conditionCheck', ... }
])
2. use condition checks¶
// ❌ Bad: No validation
await client.transactWrite([
{
type: 'update',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
updates: { balance: -100 } // Could go negative!
}
])
// ✅ Good: Validate with condition check
await client.transactWrite([
{
type: 'conditionCheck',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
condition: { balance: { gte: 100 } }
},
{
type: 'update',
key: { pk: 'ACCOUNT#123', sk: 'BALANCE' },
updates: { balance: newBalance }
}
])
3. handle retries carefully¶
// Use idempotency tokens for safe retries
import { randomUUID } from 'crypto'
async function transferMoney(from: string, to: string, amount: number) {
const requestToken = randomUUID()
const maxRetries = 3
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
await client.transactWrite(
[
// ... transfer operations
],
{
clientRequestToken: requestToken // Same token for all retries
}
)
return // Success
} catch (error) {
if (error.name === 'TransactionCanceledException') {
throw error // Don't retry condition failures
}
if (attempt === maxRetries - 1) {
throw error // Max retries reached
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt)))
}
}
}
4. prefer transactions over batch for atomicity¶
// ❌ Bad: Batch operations (not atomic)
await client.batchWrite([
{ type: 'put', item: order },
{ type: 'update', key: inventoryKey, updates: { stock: newStock } }
])
// If second operation fails, first succeeds = inconsistent state
// ✅ Good: Transaction (atomic)
await client.transactWrite([
{ type: 'put', item: order },
{ type: 'update', key: inventoryKey, updates: { stock: newStock } }
])
// Both succeed or both fail
5. monitor transaction costs¶
const client = new TableClient({
tableName: 'my-table',
client: new DynamoDBClient({ region: 'us-east-1' }),
statsConfig: {
enabled: true,
sampleRate: 1.0
}
})
// Perform transactions
await client.transactWrite(operations)
// Monitor costs
const stats = client.getStats()
console.log('Transaction WCU:', stats.operations.transactWrite?.totalWCU)
// Get recommendations
const recommendations = client.getRecommendations()
for (const rec of recommendations) {
if (rec.category === 'transaction') {
console.log(`${rec.severity}: ${rec.message}`)
}
}
Transactions vs batch operations¶
| Feature | Transactions | Batch Operations |
|---|---|---|
| Atomicity | All or nothing | Partial success possible |
| Max Items | 100 (write), 100 (read) | 25 (write), 100 (read) |
| Conditions | Supported | Not supported |
| Cost | 2x capacity units | 1x capacity units |
| Use Case | Critical consistency | Bulk operations |
| Failure Handling | All rollback | Individual retries |
Next steps¶
- Learn about Access Patterns for complex queries
- Explore Monitoring to track transaction performance
- Review Best Practices for optimization
- See Examples for complete transaction patterns