Use transactions
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
Applies to
- Aerospike Developer SDK (Java 21+ and Python 3.11+)
- Aerospike Database 8.2.0 or later, unless a section states otherwise
Transactions allow you to execute multiple operations atomically across different records. Either all operations succeed, or none do.
Except where noted, snippets on this page use the imports below. A snippet lists additional import lines only when it needs a type not shown here. When this page includes a Complete example section, that block is fully self-contained with every import required to run it.
import com.aerospike.client.sdk.AerospikeException;import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Key;import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.ResultCode;import com.aerospike.client.sdk.Session;
import java.util.UUID;import java.util.concurrent.atomic.AtomicBoolean;import asyncioimport timeimport uuid
from aerospike_sdk import Behavior, ClusterDefinition, DataSetfrom aerospike_sdk.exceptions import AerospikeErrorWhat are transactions?
Traditional Aerospike operations are atomic at the single-record level. Transactions extend atomicity across multiple records:
- Atomicity: All operations commit or all abort
- Consistency: Intermediate states are never visible
- Isolation: Concurrent transactions don’t interfere
- Durability: Committed transactions survive failures
Start a transaction
// Transactions run through Session.doInTransaction; commit happens when the block returns.DataSet accounts = DataSet.of("test", "accounts");
try { session.doInTransaction(txn -> { txn.insert(accounts) .bins("balance") .id("account-A").values(1000) .execute();
txn.insert(accounts) .bins("balance") .id("account-B").values(500) .execute(); });} catch (AerospikeException e) { // On unsupported server/namespace configs this can fail (for example "Unsupported Server Feature"). System.err.println("Transaction failed: " + e.getMessage());}📖 API reference:
DataSet.of(...)|Session.doInTransaction(...)|Session.insert(DataSet)|OperationObjectBuilder.bins(...)|IdValuesBuilder.id(...)|IdValuesRowBuilder.values(...)|ChainableQueryBuilder.execute()|AerospikeException
Use doInTransactionReturning(...) when you need a value back from the transaction:
String status = session.doInTransactionReturning(txn -> { Record account = txn.query(accounts.id("account-A")).execute().getFirstRecord(); long balance = account != null ? account.getLong("balance") : 0L; if (balance < 100) { // Nothing has been written, so the commit is a no-op. Calling txn.abort() // here instead would make doInTransactionReturning return null. return "insufficient-funds"; } txn.update(accounts.id("account-A")) .bin("balance").add(-100) .execute(); return "debited";});📖 API reference:
DataSet.id(...)|Session.doInTransactionReturning(...)|Session.update(DataSet)|Session.update(Key)|Session.query(Key)|ChainableOperationBuilder.bin(...)|ChainableQueryBuilder.bin(...)|BinBuilder.add(...)|ChainableQueryBuilder.execute()|RecordStream.getFirstRecord()|Record.getLong(...)
async with ClusterDefinition("localhost", 3000).connect() as cluster: session = cluster.create_session(Behavior.DEFAULT) accounts = DataSet.of("test", "accounts")
# Explicit-scope form: commit on clean exit, abort on exception. # Single attempt; use do_in_transaction for automatic retry. async with session.transaction() as txn: await txn.insert(accounts.id("account-A")).put({"balance": 1000}).execute() await txn.insert(accounts.id("account-B")).put({"balance": 500}).execute()
# Retrying-wrapper form (Java's doInTransaction / doInTransactionReturning): # the callable's return value is the result of do_in_transaction. async def debit(txn): row = await txn.query(accounts.id("account-A")).first() balance = row.record_or_raise().bins["balance"] if row is not None else 0 if balance < 100: # Nothing has been written, so the commit is a no-op. Calling # `await txn.abort()` here instead would end the transaction early. return "insufficient-funds" await txn.update(accounts.id("account-A")).bin("balance").add(-100).execute() return "debited"
status = await session.do_in_transaction(debit)📖 API reference:
ClusterDefinition|Cluster.create_session()|DataSet.of()|DataSet.id()|Behavior.DEFAULT|Session.transaction()|Session.do_in_transaction()|QueryBuilder.bin()|WriteSegmentBuilder.put()|WriteSegmentBuilder.bin()|QueryBuilder.execute()
doInTransaction and doInTransactionReturning (Java) and
do_in_transaction (Python) automatically retry the whole callable if the
transaction failed for what it considers a “retryable” reason. These are
typically if the transaction being managed was blocked by another
transaction. Retries occur on result codes of MRT_BLOCKED and
MRT_VERSION_MISMATCH in both SDKs, on TXN_FAILED in Java, and on a commit
that failed without being in doubt in Python. The number of attempts (default 10,
including the first) and the pause between retries (default 20 ms, with jitter)
are cluster-wide SystemSettings transaction settings, not Behavior settings.
In Java, set them with ClusterDefinition.withSystemSettings(s -> s.transactions(t -> t.numberOfAttempts(5).sleepBetweenAttempts(Duration.ofMillis(50)))).
In Python, do_in_transaction also accepts per-call max_attempts and
sleep_between_retries overrides.
CRUD within a transaction
All standard operations work within transactions:
DataSet accounts = DataSet.of("test", "accounts");DataSet ledger = DataSet.of("test", "ledger");DataSet pendingTransfers = DataSet.of("test", "pending_transfers");
session.doInTransaction(txn -> { Record account = txn.query(accounts.id("account-A")).execute().getFirstRecord(); long balance = account != null ? account.getLong("balance") : 0L;
txn.update(accounts.id("account-A")) .bin("balance").setTo(balance - 100) .execute();
txn.insert(ledger) .bins("from", "amount", "timestamp") .id(UUID.randomUUID().toString()).values("account-A", 100, System.currentTimeMillis()) .execute();
txn.delete(pendingTransfers.id("transfer-123")).execute();});📖 API reference:
DataSet.of(...)|DataSet.id(...)|Session.doInTransaction(...)|Session.insert(DataSet)|Session.update(DataSet)|Session.update(Key)|Session.query(Key)|OperationObjectBuilder.bins(...)|ChainableOperationBuilder.bin(...)|ChainableQueryBuilder.bin(...)|ChainableQueryBuilder.execute()|RecordStream.getFirstRecord()|Record.getLong(...)
async with ClusterDefinition("localhost", 3000).connect() as cluster: session = cluster.create_session(Behavior.DEFAULT) accounts = DataSet.of("test", "accounts") ledger = DataSet.of("test", "ledger") pending = DataSet.of("test", "pending_transfers")
async def debit_and_log(txn): row = await txn.query(accounts.id("account-A")).first() balance = row.record_or_raise().bins.get("balance", 0) if row else 0
await ( txn.update(accounts.id("account-A")) .bin("balance").set_to(balance - 100) .execute() )
await ( txn.insert(ledger.id(str(uuid.uuid4()))) .put({"from": "account-A", "amount": 100, "timestamp": int(time.time() * 1000)}) .execute() )
await txn.delete(pending.id("transfer-123")).execute()
await session.do_in_transaction(debit_and_log)📖 API reference:
ClusterDefinition|Cluster.create_session()|DataSet.of()|DataSet.id()|Behavior.DEFAULT|QueryBuilder.bin()|WriteSegmentBuilder.put()|WriteSegmentBuilder.set_to()|WriteSegmentBuilder.bin()|RecordResult.record_or_raise()|RecordStream.first()|RecordStream.close()|QueryBuilder.execute()
Commit and abort
Commit
Commit makes all transaction operations permanent:
// With session.doInTransaction(...), commit is performed automatically after the lambda completes.// There is no separate txn.commit() API on Session — doInTransaction commits for you.📖 API reference:
Session.doInTransaction(...)
# With `async with session.transaction() as txn:`, the transaction is# committed automatically when the block exits cleanly. You can also commit# explicitly:# status = await txn.commit()# `txn.commit()` returns a `TxnStatus`. A second call returns# `TxnStatus.ALREADY_COMMITTED`; a commit after an abort raises# `TransactionError` with `ResultCode.TXN_ALREADY_ABORTED`.📖 API reference:
Session.transaction()
Abort
Abort discards all transaction operations:
// From inside doInTransaction, call txn.abort() to abort programmatically.// Outside that API, letting an exception propagate aborts the transaction.txn.abort();📖 API reference:
TransactionalSession.abort()
# Inside an `async with session.transaction() as txn:` block:# - call `await txn.abort()` to abort programmatically, OR# - let an exception propagate and the context manager will abort for you.# `txn` is finished after this: a later operation on it raises# `TransactionError` with `ResultCode.TXN_ALREADY_ABORTED`.await txn.abort()📖 API reference:
Session.transaction()
Exclude a specific operation from the current transaction
Use notInAnyTransaction() (Java) or with_txn(None) (Python) when a specific operation must execute outside the active transaction.
DataSet accounts = DataSet.of("test", "accounts");DataSet audit = DataSet.of("test", "audit");
session.doInTransaction(txn -> { txn.update(accounts.id("account-A")) .bin("balance").add(-100) .execute();
// This write executes outside the transaction. txn.insert(audit) .bins("event", "amount") .id("audit:1").values("debit", 100) .notInAnyTransaction() .execute();});📖 API reference:
DataSet.of(...)|DataSet.id(...)|Session.doInTransaction(...)|Session.insert(DataSet)|Session.update(DataSet)|Session.update(Key)|OperationObjectBuilder.bins(...)|IdValuesBuilder.id(...)|IdValuesRowBuilder.values(...)|ChainableOperationBuilder.bin(...)|ChainableQueryBuilder.bin(...)|BinBuilder.add(...)|ChainableQueryBuilder.execute()
accounts = DataSet.of("test", "accounts")audit = DataSet.of("test", "audit")
async def debit(txn): await txn.update(accounts.id("account-A")).bin("balance").add(-100).execute()
# This write executes outside the transaction. await ( txn.insert(audit.id("audit:1")) .put({"event": "debit", "amount": 100}) .with_txn(None) .execute() )
await session.do_in_transaction(debit)📖 API reference:
DataSet.of()|DataSet.id()|Session.do_in_transaction()|WriteSegmentBuilder.put()|WriteSegmentBuilder.with_txn()|WriteSegmentBuilder.execute()
Handling in-doubt state
If the client loses connection during commit, the transaction may be in an “in-doubt” state. However, the commit automatically retries depending on policy settings for up to 15 seconds, which is more than enough time for the cluster to re-adjust.
commit is an idempotent operation, so even if it does fail after this time, it is safe to retry the commit.
Retry patterns for transactions
Implement robust retry logic for transient failures. Note that Behaviors can be configured with automatic retries, so transient failures are typically only an issue on non-idempotent writes (which cannot easily be retried).
public void transferWithRetry(Session session, Key acctKey, int maxAttempts) throws InterruptedException { AtomicBoolean timeoutFailure = new AtomicBoolean(); for (int attempt = 1; attempt <= maxAttempts; attempt++) { timeoutFailure.set(false); try { session.doInTransaction(txn -> { try { // Non-idempotent operations cannot handle `inDoubt` well txn.update(acctKey).bin("balance").add(500).execute(); } catch (AerospikeException.Timeout timeout) { timeoutFailure.set(true); txn.abort(); } }); if (timeoutFailure.get()) { Thread.sleep((long) Math.pow(2, attempt) * 10L); continue; } return; } catch (AerospikeException e) { if ((e.getResultCode() == ResultCode.MRT_BLOCKED || e.getResultCode() == ResultCode.MRT_VERSION_MISMATCH || e.getResultCode() == ResultCode.TXN_FAILED) && attempt < maxAttempts) { Thread.sleep((long) Math.pow(2, attempt) * 10L); continue; } throw e; } }}📖 API reference:
Session.doInTransaction(...)|TransactionalSession.abort()|Session.update(DataSet)|ChainableOperationBuilder.bin(...)|ChainableQueryBuilder.bin(...)|BinBuilder.add(...)|AerospikeException
# do_in_transaction already retries transient conflicts (MRT_BLOCKED,# MRT_VERSION_MISMATCH) and failed commits, as set by the cluster's# TransactionSettings or its max_attempts / sleep_between_retries arguments.# Wrap it for application-level backoff on top of that.async def with_retry(operation, max_retries=3): attempt = 0 while True: try: return await operation() except AerospikeError as exc: attempt += 1 if attempt >= max_retries: raise # Exponential backoff between transactional retries await asyncio.sleep((2 ** attempt) * 0.01)
# Usage: pass a coroutine function that runs the transaction.async def transfer_funds(session, accounts, from_id, to_id, amount): async def op(txn): await txn.update(accounts.id(from_id)).bin("balance").add(-amount).execute() await txn.update(accounts.id(to_id)).bin("balance").add(amount).execute()
return await session.do_in_transaction(op)📖 API reference:
DataSet.id()|QueryBuilder.bin()|WriteSegmentBuilder.bin()|QueryBuilder.execute()
Complete example
This example is self-contained—it lists every import needed to run standalone.
import com.aerospike.client.sdk.AerospikeException;import com.aerospike.client.sdk.Cluster;import com.aerospike.client.sdk.ClusterDefinition;import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.Session;import com.aerospike.client.sdk.policy.Behavior;
public class TransactionTransferExample { public static void main(String[] args) { try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) { Session session = cluster.createSession(Behavior.DEFAULT); DataSet accounts = DataSet.of("test", "accounts");
String from = "txn-example-A"; String to = "txn-example-B"; long transfer = 100L;
// Cleanup + seed so the example is repeatable. session.delete(accounts.ids(from, to)).withDurableDelete().execute(); session.insert(accounts) .bins("owner", "balance") .id(from).values("Alice", 1000L) .id(to).values("Bob", 300L) .execute();
// Atomic transfer. try { session.doInTransaction(txn -> { Record fromRec = txn.query(accounts.id(from)).execute().getFirstRecord(); Record toRec = txn.query(accounts.id(to)).execute().getFirstRecord(); long fromBal = fromRec != null ? fromRec.getLong("balance") : 0L; long toBal = toRec != null ? toRec.getLong("balance") : 0L;
if (fromBal < transfer) { txn.abort(); return; }
txn.update(accounts.id(from)) .bin("balance").add(-transfer) .execute();
txn.update(accounts.id(to)) .bin("balance").add(transfer) .execute(); });
// Verify resulting balances. Record afterFrom = session.query(accounts.id(from)).execute().getFirstRecord(); Record afterTo = session.query(accounts.id(to)).execute().getFirstRecord(); System.out.println("From balance: " + (afterFrom != null ? afterFrom.getLong("balance") : null)); System.out.println("To balance: " + (afterTo != null ? afterTo.getLong("balance") : null)); } catch (AerospikeException e) { System.err.println("Transaction failed: " + e.getMessage()); System.err.println("Ensure this namespace supports transactions (SC namespace on server 8.0+)."); } } }}📖 API reference:
ClusterDefinition(String,int)|ClusterDefinition.connect()|Cluster.createSession(Behavior)|Cluster.close()|DataSet.of(...)|DataSet.ids(...)|Session.doInTransaction(...)|TransactionalSession.abort()|Session.insert(DataSet)|Session.update(DataSet)|Session.update(Key)|Session.delete(List)|Session.delete(Key)|ChainableNoBinsBuilder.withDurableDelete()|Session.query(Key)|OperationObjectBuilder.bins(...)|IdValuesBuilder.id(...)|IdValuesRowBuilder.values(...)|ChainableOperationBuilder.bin(...)|ChainableQueryBuilder.bin(...)|ChainableQueryBuilder.execute()|RecordStream.getFirstRecord()|Record.getLong(...)|AerospikeException
import asynciofrom aerospike_sdk import Behavior, ClusterDefinition, DataSetfrom aerospike_sdk.exceptions import AerospikeError
async def main(): async with ClusterDefinition("localhost", 3000).connect() as cluster: session = cluster.create_session(Behavior.DEFAULT) # Transactions require an SC namespace on Aerospike Server 8.0+. accounts = DataSet.of("test", "accounts") from_id = "txn-example-A" to_id = "txn-example-B" transfer = 100
# Cleanup + seed so the example is repeatable. try: await session.delete(accounts.id(from_id)).execute() await session.delete(accounts.id(to_id)).execute() except AerospikeError: pass await session.insert(accounts.id(from_id)).put({"owner": "Alice", "balance": 1000}).execute() await session.insert(accounts.id(to_id)).put({"owner": "Bob", "balance": 300}).execute()
# Atomic transfer: do_in_transaction commits on clean return and # automatically retries on retryable failures (MRT_BLOCKED, ...). async def transfer_op(txn): from_stream = await txn.query(accounts.id(from_id)).execute() to_stream = await txn.query(accounts.id(to_id)).execute() from_row = await from_stream.first() to_row = await to_stream.first() from_stream.close() to_stream.close()
from_bal = from_row.record_or_raise().bins.get("balance", 0) if from_row else 0 to_bal = to_row.record_or_raise().bins.get("balance", 0) if to_row else 0 if from_bal < transfer: await txn.abort() return
await txn.update(accounts.id(from_id)).bin("balance").add(-transfer).execute() await txn.update(accounts.id(to_id)).bin("balance").add(transfer).execute()
try: await session.do_in_transaction(transfer_op) except AerospikeError as exc: print(f"Transaction failed: {exc}") print("Ensure this namespace supports transactions (SC namespace on server 8.0+).") return
# Verify resulting balances. stream = await session.query(accounts.id(from_id), accounts.id(to_id)).execute() async for row in stream: record = row.record_or_raise() print(f"{row.key.value}: {record.bins.get('balance')}") stream.close()
if __name__ == "__main__": asyncio.run(main())📖 API reference:
ClusterDefinition|Cluster.create_session()|DataSet.of()|DataSet.id()|Behavior.DEFAULT|Session.query()|Session.insert()|Session.delete()|QueryBuilder.bin()|WriteSegmentBuilder.put()|WriteSegmentBuilder.bin()|RecordResult.record_or_raise()|RecordStream.first()|RecordStream.close()|QueryBuilder.execute()|WriteSegmentBuilder.execute()
Transaction limitations
| Limitation | Value | Notes |
|---|---|---|
| Max write commands per transaction | 4096 | MRT_TOO_MANY_WRITES when exceeded |
| Max transaction duration | 10s | Default timeout |
| Namespaces | SC only | Strong consistency required |
| Operations | Point-key and batch CRUD | Dataset queries and scans, and background tasks, are not transactional |
Best practices
- Keep transactions short — Minimize time between begin and commit
- Limit scope — Include only records that must be atomic
- Handle conflicts — Implement retry logic for concurrent modifications
- Use idempotent operations — Enables safe retries
- Monitor in-doubt state — Log and alert on in-doubt transactions
Next steps
Error Handling
Handle transaction errors gracefully.
Batch Operations
For non-transactional bulk operations.