DynamoDB TransactWrite: All-or-Nothing Writes Explained

DynamoDB TransactWrite: All-or-Nothing Writes Explained
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:

  • Put
  • Update
  • Delete
  • ConditionCheck

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

  1. Reduce stock only if enough is available
  2. 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:

  1. Read version = 7
  2. Update only if version is still 7
  3. 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:

  1. Re-read latest state
  2. Retry 2–3 times (for contention)
  3. 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
}
  • expiresAt for app logic
  • ttl for 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.


Further reading

Read more