Transactions
This page documents TableTheory’s public write-transaction surfaces, which use the actual DynamoDB TransactWriteItems API. There is no app-level lock, no optimistic-concurrency-over-HTTP simulation, and no hidden retry sleep loop.
What a transaction guarantees
- Atomicity across all items in the group: either every write applies or none.
- Condition evaluation is server-side: each write item carries its own conditional expression, and a single failed condition aborts the whole transaction.
- Optimistic-lock composition: a versioned item in the group asserts its expected version; a version mismatch aborts the transaction atomically.
- Encryption composition: encrypted fields are encrypted before the transaction is submitted; a KMS failure aborts before any write hits DynamoDB.
- Cross-runtime parity: a write transaction submitted from Python sees the same atomicity guarantees as one submitted from Go.
Write transaction limits to know
| Limit | Value / behavior |
|---|---|
DynamoDB TransactWriteItems item limit |
100 items per service call |
Go core.TransactionBuilder operation cap |
25 operations in the current public builder |
TypeScript TheorydbClient.transactWrite cap |
DynamoDB enforces service-call limits |
Python Table.transact_write action cap |
100 actions |
| Maximum DynamoDB transaction payload size | 4 MB |
| Items addressed across multiple tables | allowed by DynamoDB |
| Same item appearing twice in one call | not allowed by DynamoDB |
TableTheory does not auto-chunk write transactions across multiple
TransactWriteItems calls — that would silently break atomicity. If you exceed
the runtime guard or DynamoDB service limit, redesign the access pattern instead
of splitting one logical transaction into multiple calls.
Go
// Conditional writes composed with a peer create inside one TransactWriteItems call.
err := db.TransactWrite(ctx, func(tx core.TransactionBuilder) error {
tx.UpdateWithBuilder(&fromAcct, func(u core.UpdateBuilder) error {
u.Set("Balance", fromBal)
u.Add("Version", int64(1))
u.ConditionVersion(fromVersion)
return nil
})
tx.UpdateWithBuilder(&toAcct, func(u core.UpdateBuilder) error {
u.Set("Balance", toBal)
u.Add("Version", int64(1))
u.ConditionVersion(toVersion)
return nil
})
tx.Create(&audit)
return nil
})
Use
db.TransactWrite(ctx, func(core.TransactionBuilder) error)or the fluentdb.Transact()builder followed byExecute()for DynamoDB transactions. The olderdb.Transaction(func(*core.Tx) error)helper is only a compatibility wrapper and is not the canonical full-transaction API.
TypeScript
TheorydbClient.transactWrite(actions: TransactAction[]) accepts a list of
{ kind: 'put' | 'update' | 'delete' | 'condition', model, ... } actions.
Update actions provide key plus either a raw updateExpression or an updateFn
that uses the UpdateBuilder DSL.
await db.transactWrite([
{
kind: 'update',
model: 'Account',
key: fromKey,
updateFn: (u) => {
u.set('balance', fromBal).add('version', 1).conditionVersion(fromVersion);
},
},
{
kind: 'update',
model: 'Account',
key: toKey,
updateFn: (u) => {
u.set('balance', toBal).add('version', 1).conditionVersion(toVersion);
},
},
{
kind: 'put',
model: 'Audit',
item: auditItem,
ifNotExists: true,
},
]);
See ts/src/transaction.ts for the full TransactAction shape.
Python
Table.transact_write(actions) accepts a list of dataclass actions —
TransactPut, TransactUpdate, TransactDelete, TransactConditionCheck — all importable from theorydb_py.
from theorydb_py import TransactPut, TransactUpdate
table.transact_write([
TransactUpdate(
pk="ACCOUNT#A", sk="v1",
updates={"balance": from_bal},
),
TransactUpdate(
pk="ACCOUNT#B", sk="v1",
updates={"balance": to_bal},
),
TransactPut(item=audit_item),
])
See py/src/theorydb_py/transaction.py for the full action-dataclass shapes.
Common patterns
- Transferring state between two items: update both with conditional expressions.
- Atomic create-if-absent of multiple items: create each with
attribute_not_exists(pk). - Composite invariants across a parent and its children: condition-check the parent’s version while writing children atomically.
Anti-patterns
- Don’t simulate transactions with successive single writes. DynamoDB transactions exist for exactly this reason; rolling your own loses atomicity.
- Don’t catch the abort error and retry without recomputing. A condition failure means your snapshot is stale; reload state before the next attempt.
- Don’t put the same item twice in one transaction. DynamoDB rejects it. Combine the changes locally first.
Related
- Optimistic Locking — versioned writes inside and outside transactions
- Core Patterns — end-to-end transaction recipes
- FaceTheory · ISR Transaction Recipes — production transactions used by the ISR cache layer