ACID transactions with MVCC snapshot isolation. Multi-collection operations with automatic commit/rollback.
Every ALOS DB transaction provides full ACID guarantees:
All operations commit together or none do. No partial writes.
Database moves from one valid state to another. Indexes stay in sync.
MVCC snapshot isolation. Transactions don’t see each other’s uncommitted data.
Committed data survives crashes when --sync-mode sync is enabled.
The closure pattern automatically manages commit and rollback:
err := db.Transaction(func(tx alosdbclient.TransactionInterface) error { users := tx.Collection("users") // Insert a user id, err := users.InsertOne(alosdbclient.Document{ "name": "Alice", "email": "alice@example.com", }) if err != nil { return err // triggers rollback } // Update the user err = users.UpdateOne( alosdbclient.Document{"_id": id}, alosdbclient.Document{"$set": alosdbclient.Document{"verified": true}}, ) if err != nil { return err // triggers rollback } return nil // triggers commit }) if err != nil { log.Printf("Transaction failed: %v", err) }
Return nil → auto-commit. Return error → auto-rollback. Panic → auto-rollback. You never need to call Commit or Rollback explicitly.
For cases where you need explicit control over the transaction lifecycle:
tx := db.BeginTransaction() users := tx.Collection("users") _, err := users.InsertOne(alosdbclient.Document{ "name": "Bob", "age": 25, }) if err != nil { tx.Rollback() log.Fatal(err) } // Commit all changes if err := tx.Commit(); err != nil { log.Fatal("commit failed:", err) } // Get the transaction ID for logging fmt.Println("Committed TX:", tx.GetID())
With manual transactions, you must call either Commit() or
Rollback(). Forgetting to do so will leave the transaction open. Prefer the
closure pattern when possible.
A single transaction can span multiple collections. All operations commit or rollback together:
err := db.Transaction(func(tx alosdbclient.TransactionInterface) error { users := tx.Collection("users") accounts := tx.Collection("accounts") auditLog := tx.Collection("audit_log") // Create user userID, err := users.InsertOne(alosdbclient.Document{ "name": "Alice", "email": "alice@example.com", }) if err != nil { return err } // Create account for user _, err = accounts.InsertOne(alosdbclient.Document{ "user_id": userID, "balance": 0.0, "status": "active", }) if err != nil { return err } // Write audit log entry _, err = auditLog.InsertOne(alosdbclient.Document{ "action": "user_created", "user_id": userID, }) return err })
If any operation fails, all three collections are rolled back to their state before the transaction began.
A common pattern: read a document, modify it, and write it back — all atomically. With ALOS DB's interactive transactions, FindOne inside a transaction reads from the transaction's snapshot, and concurrent modifications are detected at commit time.
// Transfer money between accounts err := db.Transaction(func(tx alosdbclient.TransactionInterface) error { accounts := tx.Collection("accounts") // Read source account (from transaction snapshot) src, err := accounts.FindOne(alosdbclient.Document{"_id": sourceID}) if err != nil { return fmt.Errorf("source not found: %w", err) } // Check balance balance := src["balance"].(float64) if balance < amount { return fmt.Errorf("insufficient funds: %.2f < %.2f", balance, amount) } // Debit source err = accounts.UpdateOne( alosdbclient.Document{"_id": sourceID}, alosdbclient.Document{"$set": alosdbclient.Document{ "balance": balance - amount, }}, ) if err != nil { return err } // Credit destination dst, err := accounts.FindOne(alosdbclient.Document{"_id": destID}) if err != nil { return fmt.Errorf("destination not found: %w", err) } dstBalance := dst["balance"].(float64) return accounts.UpdateOne( alosdbclient.Document{"_id": destID}, alosdbclient.Document{"$set": alosdbclient.Document{ "balance": dstBalance + amount, }}, ) })
$gteFor extra safety on balance checks, put the condition directly in the UpdateOne filter. The server re-checks the filter at commit time under the document lock, so even if another transaction changes the balance between your read and commit, the update will abort safely:
// Atomic conditional debit — aborts if balance is insufficient at commit time err := db.Transaction(func(tx alosdbclient.TransactionInterface) error { users := tx.Collection("users") // The $gte filter is re-checked at commit under the document lock err := users.UpdateOne( alosdbclient.Document{ "email": "alice@example.com", "balance": alosdbclient.Document{"$gte": alosdbclient.Int64(50)}, }, alosdbclient.Document{"$sub": alosdbclient.Document{"balance": alosdbclient.Int64(50)}}, ) if err != nil { // "document not found" means the filter didn't match (insufficient balance) return fmt.Errorf("insufficient balance") } return nil })
Best of both worlds: The FindOne gives you the current value for business logic, and the $gte in the UpdateOne filter guarantees the commit only succeeds if the balance is still sufficient. This prevents race conditions even under high concurrency.
Errors inside a transaction trigger automatic rollback. Handle them cleanly:
err := db.Transaction(func(tx alosdbclient.TransactionInterface) error { users := tx.Collection("users") // This might fail if user doesn't exist user, err := users.FindOne(alosdbclient.Document{"email": email}) if err != nil { return fmt.Errorf("user lookup: %w", err) } // Business logic check if user["status"] == "suspended" { return fmt.Errorf("user %s is suspended", email) } return nil }) if err != nil { // Transaction was rolled back — handle the error log.Printf("Transaction failed: %v\n", err) } else { // Transaction was committed successfully log.Println("Transaction committed") }
ALOS DB uses Multi-Version Concurrency Control (MVCC) for transaction isolation. When a transaction begins, it takes a snapshot of the database:
Time ──────────────────────────────────────────►
TX-1: BEGIN ─── Read(A) ─── Write(A) ─── COMMIT
│ │
│ TX-2 writes A here │
│ (TX-1 doesn't see it) │
│ │
TX-2: BEGIN ──── Write(A) ── COMMIT │
│
TX-1 sees its own snapshot ─┘
MVCC means readers never block writers and writers never block readers. Only write-write conflicts on the same document require coordination.
db.Transaction(fn)
handles commit/rollback automatically and is harder to misuse--sync-mode sync
ensures committed transactions survive crashes$gte
in your UpdateOne filter so the server re-checks the condition at commit time
under the document lock. This prevents race conditions even if MVCC is disabled.