Transactions
Manage atomic, durable, isolated transactions using ekoDB's REST API with support for savepoints and full rollback capabilities.
ekoDB transactions are buffered and isolated. Statements you issue with a transaction_id are staged server-side and applied atomically only at commit, so they are invisible to other transactions until then (no dirty reads) and visible to your own reads that pass the same transaction_id (read-your-writes). Isolation is enforced by optimistic concurrency over per-record version stamps (MVCC): at commit, ekoDB validates that the records this transaction read or wrote have not been changed under it by another committed transaction — how much it validates depends on the isolation level. If they have, the commit is rejected with a retryable conflict (HTTP 409) and nothing is applied. This applies to REST/client transactions and to transactional stored functions alike. Atomicity (all-or-nothing; a rollback or failure discards the staged writes) and durability (WAL) are guaranteed as before.
For detailed information about transactions architecture, isolation levels, and best practices, see the Transactions Architecture Reference.
When using the REST API directly (without client libraries), be aware of these important limitations:
- Manual Transaction ID Management - You must track and include the
transaction_idwith every operation within a transaction - Connection Stateless - REST is stateless; transaction state is maintained server-side with timeout expiration
- No Automatic Rollback - If your client crashes, the transaction will remain pending until timeout (default: 5 minutes)
- Network Failures - A failed commit response doesn't guarantee the commit failed; use idempotency keys for critical operations
Recommendation: Use the official client libraries which handle transaction lifecycle automatically.
Isolation Levels
The isolation level selects how much ekoDB validates at commit. ekoDB uses optimistic concurrency: no locks are held across statements, and a conflict surfaces as a retryable 409 at commit rather than blocking. The level is resolved per transaction as the value you pass, else the server-wide default_isolation_level setting (ReadUncommitted out of the box; the official client libraries send ReadCommitted by default). It can be changed at runtime via /api/config.
- ReadUncommitted — no conflict detection; concurrent commits are last-writer-wins. Cheapest, and the server default unless
default_isolation_levelis changed or a level is passed. - ReadCommitted — lost-update prevention: the commit is rejected if a record this transaction wrote was changed by another committed transaction since this transaction observed it.
- RepeatableRead — adds read-set validation: the commit is rejected if a record this transaction read (not only one it wrote) changed before commit. Prevents non-repeatable reads / read skew. Phantoms are still possible.
- Serializable — strongest: adds collection-membership validation on top — the commit is rejected if the membership of any collection this transaction queried changed before commit (a concurrently committed insert / delete / update that shifts a predicate's result set). Prevents phantoms, and with them write skew; the committed history is serializable.
ekoDB validates version stamps at commit; it does not take per-statement read snapshots. A level's guarantee is "a transaction that would exhibit the anomaly aborts at commit," not lock-based read isolation during execution — reads always observe live committed data. The ladder is strict: each level prevents a superset of the anomalies below it (lost updates → non-repeatable reads → phantoms). Phantom protection (Serializable) is gated, so it costs other writers nothing unless a Serializable transaction is in flight. Isolation is also coherent with direct (non-transactional) writes: every durable write bumps the same per-record version, so a concurrent transaction detects it.
Ripple Propagation in Transactions
Transactional writes ripple at commit. Each write staged inside a transaction carries its own bypass_ripple intent (default false = propagate to peers; true = suppress), and propagation happens once the transaction commits. Staged writes are never rippled before commit, and a rolled-back or conflicted (409) transaction ripples nothing. A single transaction may mix per-operation choices.
Begin Transaction
Start a new transaction with optional isolation level and timeout.
- Client Libraries (Recommended)
- Direct API
- 🦀 Rust
- 🐍 Python
- 📘 TypeScript
- 📦 JavaScript
- 🟣 Kotlin
- 🔷 Go
let tx_id = client.begin_transaction(Some(TransactionOptions {
isolation_level: IsolationLevel::Serializable,
timeout_seconds: Some(600),
})).await?;
println!("{}", tx_id); // 'tx-abc-123'
tx_id = await client.begin_transaction(
isolation_level='Serializable'
)
print(tx_id) # 'tx-abc-123'
const txId = await client.beginTransaction({
isolation_level: "Serializable",
});
console.log(txId); // 'tx-abc-123'
const txId = await client.beginTransaction({
isolation_level: "Serializable",
});
console.log(txId); // 'tx-abc-123'
val txId = client.beginTransaction(
isolationLevel = "Serializable"
)
println(txId) // 'tx-abc-123'
txId, err := client.BeginTransaction(ekodb.TransactionOptions{
IsolationLevel: "Serializable",
})
fmt.Println(txId) // 'tx-abc-123'
curl -X POST https://{EKODB_API_URL}/api/transactions \
-H "Authorization: Bearer {YOUR_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"isolation_level": "Serializable",
"timeout_seconds": 600
}'
# Response
{
"transaction_id": "tx-abc-123",
"isolation_level": "Serializable",
"created_at": 1699814400
}
Prefer a client library? See examples in Rust, Python, TypeScript, Go, or Kotlin
Want to use the REST API directly? See examples in JavaScript, Python, Go, or Rust
Parameters:
isolation_level(optional) -ReadUncommitted,ReadCommitted,RepeatableRead,Serializabletimeout_seconds(optional) - Default: 300 (5 minutes)
Commit Transaction
Commit all operations within the transaction.
- Client Libraries (Recommended)
- Direct API
- 🦀 Rust
- 🐍 Python
- 📘 TypeScript
- 📦 JavaScript
- 🟣 Kotlin
- 🔷 Go
client.commit_transaction(&tx_id).await?;
await client.commit_transaction(tx_id)
await client.commitTransaction(txId);
await client.commitTransaction(txId);
client.commitTransaction(txId)
err := client.CommitTransaction(txId)
curl -X POST https://{EKODB_API_URL}/api/transactions/{transaction_id}/commit \
-H "Authorization: Bearer {YOUR_API_TOKEN}"
# Response
{
"status": "committed",
"transaction_id": "tx-abc123",
"operations_count": 5
}
Rollback Transaction
Rollback all operations and abort the transaction.
- Client Libraries (Recommended)
- Direct API
- 🦀 Rust
- 🐍 Python
- 📘 TypeScript
- 📦 JavaScript
- 🟣 Kotlin
- 🔷 Go
client.rollback_transaction(&tx_id).await?;
await client.rollback_transaction(tx_id)
await client.rollbackTransaction(txId);
await client.rollbackTransaction(txId);
client.rollbackTransaction(txId)
err := client.RollbackTransaction(txId)
curl -X POST https://{EKODB_API_URL}/api/transactions/{transaction_id}/rollback \
-H "Authorization: Bearer {YOUR_API_TOKEN}"
# Response
{
"status": "rolled_back",
"transaction_id": "tx-abc123",
"operations_rolled_back": 5
}
Create Savepoint
Create a savepoint for partial rollback within a transaction.
- Client Libraries (Recommended)
- Direct API
- 🦀 Rust
- 🐍 Python
- 📘 TypeScript
- 📦 JavaScript
- 🟣 Kotlin
- 🔷 Go
client.create_savepoint(&tx_id, "checkpoint1").await?;
await client.create_savepoint(tx_id, 'checkpoint1')
await client.createSavepoint(txId, "checkpoint1");
await client.createSavepoint(txId, "checkpoint1");
client.createSavepoint(txId, "checkpoint1")
err := client.CreateSavepoint(txId, "checkpoint1")
curl -X POST https://{EKODB_API_URL}/api/transactions/{transaction_id}/savepoints \
-H "Authorization: Bearer {ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "checkpoint1"
}'
# Response
{
"status": "created",
"transaction_id": "tx-abc123",
"savepoint_name": "checkpoint1"
}
Rollback to Savepoint
Rollback to a specific savepoint, undoing operations after it.
- Client Libraries (Recommended)
- Direct API
- 🦀 Rust
- 🐍 Python
- 📘 TypeScript
- 📦 JavaScript
- 🟣 Kotlin
- 🔷 Go
client.rollback_to_savepoint(&tx_id, "checkpoint1").await?;
await client.rollback_to_savepoint(tx_id, 'checkpoint1')
await client.rollbackToSavepoint(txId, "checkpoint1");
await client.rollbackToSavepoint(txId, "checkpoint1");
client.rollbackToSavepoint(txId, "checkpoint1")
err := client.RollbackToSavepoint(txId, "checkpoint1")
curl -X POST https://{EKODB_API_URL}/api/transactions/{transaction_id}/savepoints/{savepoint_name}/rollback \
-H "Authorization: Bearer {ADMIN_TOKEN}"
# Response
{
"status": "rolled_back",
"transaction_id": "tx-abc123",
"savepoint_name": "checkpoint1",
"operations_rolled_back": 3
}
Release Savepoint
Remove a savepoint that's no longer needed.
- Client Libraries (Recommended)
- Direct API
- 🦀 Rust
- 🐍 Python
- 📘 TypeScript
- 📦 JavaScript
- 🟣 Kotlin
- 🔷 Go
client.release_savepoint(&tx_id, "checkpoint1").await?;
await client.release_savepoint(tx_id, 'checkpoint1')
await client.releaseSavepoint(txId, "checkpoint1");
await client.releaseSavepoint(txId, "checkpoint1");
client.releaseSavepoint(txId, "checkpoint1")
err := client.ReleaseSavepoint(txId, "checkpoint1")
curl -X DELETE https://{EKODB_API_URL}/api/transactions/{transaction_id}/savepoints/{savepoint_name} \
-H "Authorization: Bearer {ADMIN_TOKEN}"
# Response
{
"status": "released",
"transaction_id": "tx-abc123",
"savepoint_name": "checkpoint1"
}
Get Transaction Status
Check the current status of a transaction.
- Client Libraries (Recommended)
- Direct API
- 🦀 Rust
- 🐍 Python
- 📘 TypeScript
- 📦 JavaScript
- 🟣 Kotlin
- 🔷 Go
let status = client.get_transaction_status(&tx_id).await?;
println!("{:?}", status);
status = await client.get_transaction_status(tx_id)
print(status['state'])
print(status['operations_count'])
const status = await client.getTransactionStatus(txId);
console.log(status.state);
console.log(status.operations_count);
const status = await client.getTransactionStatus(txId);
console.log(status.state);
console.log(status.operations_count);
val status = client.getTransactionStatus(txId)
println(status)
status, err := client.GetTransactionStatus(txId)
fmt.Println(status)
curl https://{EKODB_API_URL}/api/transactions/{transaction_id} \
-H "Authorization: Bearer {ADMIN_TOKEN}"
# Response
{
"transaction_id": "tx-abc123",
"state": "Active",
"isolation_level": "Serializable",
"operations_count": 5,
"savepoints_count": 2,
"created_at": 1699814400,
"last_activity": 1699814450,
"timeout_seconds": 600
}
List Active Transactions
List all currently active transactions.
- Client Libraries (Recommended)
- Direct API
- 📘 TypeScript
- 📦 JavaScript
- 🐍 Python
- 🦀 Rust
- 🟣 Kotlin
- 🔷 Go
const transactions = await client.listActiveTransactions();
console.log(transactions);
/*
[
{ transaction_id: 'tx-abc-123', isolation_level: 'Serializable', created_at: 1699814400 },
{ transaction_id: 'tx-def-456', isolation_level: 'ReadCommitted', created_at: 1699814500 }
]
*/
const transactions = await client.listActiveTransactions();
console.log(transactions);
transactions = await client.list_active_transactions()
print(transactions)
# [{'transaction_id': 'tx-abc-123', ...}]
let transactions = client.list_active_transactions().await?;
println!("{:?}", transactions);
val transactions = client.listActiveTransactions()
println(transactions)
transactions, err := client.ListActiveTransactions()
fmt.Println(transactions)
curl https://{EKODB_API_URL}/api/transactions \
-H "Authorization: Bearer {ADMIN_TOKEN}"
# Response
{
"transactions": [
{
"transaction_id": "tx-abc123",
"state": "Active",
"isolation_level": "Serializable",
"operations_count": 5,
"savepoints_count": 2,
"created_at": 1699814400,
"last_activity": 1699814450,
"timeout_seconds": 600
},
{
"transaction_id": "tx-def456",
"state": "Active",
"isolation_level": "ReadCommitted",
"operations_count": 2,
"savepoints_count": 0,
"created_at": 1699814500,
"last_activity": 1699814510,
"timeout_seconds": 300
}
],
"count": 2
}
Complete Example
Here's a complete example of a multi-step transaction with savepoints:
# 1. Begin transaction
curl -X POST https://{EKODB_API_URL}/api/transactions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {YOUR_API_TOKEN}" \
-d '{"isolation_level": "Serializable", "timeout_seconds": 600}'
# Response: {"transaction_id": "tx-001"}
# 2. Create initial savepoint
curl -X POST https://{EKODB_API_URL}/api/transactions/tx-001/savepoints \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {YOUR_API_TOKEN}" \
-d '{"name": "start"}'
# 3. Debit source account
curl -X PUT https://{EKODB_API_URL}/api/update/accounts/ACC001 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {YOUR_API_TOKEN}" \
-d '{"balance": 900}'
# 4. Create savepoint after debit
curl -X POST https://{EKODB_API_URL}/api/transactions/tx-001/savepoints \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {YOUR_API_TOKEN}" \
-d '{"name": "after_debit"}'
# 5. Credit destination account
curl -X PUT https://{EKODB_API_URL}/api/update/accounts/ACC002 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {YOUR_API_TOKEN}" \
-d '{"balance": 1100}'
# 6. If credit fails, rollback to after_debit
# curl -X POST https://{EKODB_API_URL}/api/transactions/tx-001/savepoints/after_debit/rollback \
# -H "Authorization: Bearer {YOUR_API_TOKEN}"
# Then retry credit operation...
# 7. Commit transaction
curl -X POST https://{EKODB_API_URL}/api/transactions/tx-001/commit \
-H "Authorization: Bearer {YOUR_API_TOKEN}"
Best Practices
Choose an Isolation Level
Pick the level by how strictly the commit must validate (see Isolation Levels). Higher levels reject more conflicts at commit, so be ready to retry a transaction that fails with a 409.
| Use Case | Level | Why |
|---|---|---|
| Analytics/Reporting | ReadUncommitted | Performance over consistency; no validation |
| General CRUD | ReadCommitted | Prevents lost updates; client default |
| Reads must stay valid | RepeatableRead | Also prevents non-repeatable reads / read skew |
| Banking/Critical | Serializable | Also prevents phantoms (and write skew) |
Use Savepoints for Multi-Stage Operations
Savepoints allow partial rollback without aborting the entire transaction:
# Create savepoint before risky operation
POST /api/transactions/{id}/savepoints
{"name": "before_risky_op"}
# Perform risky operation
PUT /api/update/...
# If operation fails, rollback to savepoint
POST /api/transactions/{id}/savepoints/before_risky_op/rollback
# Retry or continue with alternative approach
Set Appropriate Timeouts
// Short transaction (1 minute)
{"timeout_seconds": 60}
// Long-running workflow (30 minutes)
{"timeout_seconds": 1800}
// Critical operation (5 minutes, default)
{"timeout_seconds": 300}
Handle Errors Gracefully
Always implement proper error handling with rollback:
# Begin transaction
curl -X POST .../api/transactions ...
# -> {"transaction_id": "tx-001"}
# Try operations
if operation_fails; then
# Rollback on error
curl -X POST .../api/transactions/tx-001/rollback ...
else
# Commit on success
curl -X POST .../api/transactions/tx-001/commit ...
fi
Monitor Active Transactions
Track and debug active transactions:
# List all active transactions
GET /api/transactions
# Check specific transaction status
GET /api/transactions/{id}
# Useful for:
# - Debugging stuck transactions
# - Monitoring long-running operations
# - Identifying transaction bottlenecks
Use Cases
Financial Transfers
Multi-account updates with atomicity guarantees:
# Begin transaction
POST /api/transactions
{"isolation_level": "Serializable"}
# Debit source
PUT /api/update/accounts/{from_id}
{"$decrement": {"balance": 100}}
# Credit destination
PUT /api/update/accounts/{to_id}
{"$increment": {"balance": 100}}
# Commit both or rollback both
POST /api/transactions/{id}/commit
E-Commerce Orders
Order creation with inventory reservation:
# Begin transaction
POST /api/transactions
{"isolation_level": "RepeatableRead"}
# Create savepoint before inventory check
POST /api/transactions/{id}/savepoints
{"name": "before_inventory"}
# Reserve inventory
PUT /api/update/inventory/{product_id}
{"$decrement": {"stock": 1}}
# If insufficient stock, rollback to savepoint
# Otherwise create order
POST /api/insert/orders
{...}
# Commit transaction
POST /api/transactions/{id}/commit
Batch Processing
Process multiple records with error recovery:
# Begin transaction
POST /api/transactions
# Process records in batches
for batch in batches:
# Create savepoint before batch
POST /api/transactions/{id}/savepoints
{"name": "batch_{i}"}
# Process batch
POST /api/batch/insert/...
# If batch fails, rollback and continue
if error:
POST /api/transactions/{id}/savepoints/batch_{i}/rollback
continue
# Commit all successful batches
POST /api/transactions/{id}/commit
Related Documentation
- Transactions Architecture Reference - Detailed architecture, isolation levels, and best practices
- Functions Architecture - Function-based transactions (REST API)
- Functions via Client Libraries - Function-based transactions with client SDKs
- Basic Operations - Standard CRUD operations
- Batch Operations - Bulk operations within transactions
- White Paper - ACID compliance and architecture
Example Code
Direct HTTP/REST API Examples
Raw HTTP examples demonstrating the REST API directly:
- JavaScript:
transactions.js - Python:
transactions.py - Go:
transactions.go - Rust:
transactions.rs
Client Library Examples
Production-ready examples using official client libraries:
- Rust:
client_transactions.rs - Python:
client_transactions.py - TypeScript:
client_transactions.ts - Go:
client_transactions.go - Kotlin:
ClientTransactions.kt - JavaScript:
client_transactions.js