Patterns · 2 of 4

Atomic transactions

Every procedure call is one OCC transaction. All steps either commit together or roll back together. Two rules to remember.

Rule 1: abort rolls back everything

proc atomic_demo(account_id, amount) {
    update("accounts", {_id: account_id}, {$inc: {balance: -amount}})
    insert("audit", {action: "debit", amount: amount})
    if amount > 10000 {
        abort "amount over daily limit"
    }
    return {ok: true}
}

If the abort fires, the update AND the insert are both rolled back.

Rule 2: merge per-document updates

OCC validates a version per document, not per field. Two updates on the same document inside one proc will conflict.

WRONG — will conflict

update("accounts", {_id: id}, {$inc: {balance: -amount}})
update("accounts", {_id: id}, {$inc: {tx_count: 1}})
update("accounts", {_id: id}, {$set: {last_tx: "now"}})

RIGHT — single update with combined operators

update("accounts", {_id: id}, {
    $inc: {balance: -amount, tx_count: 1},
    $set: {last_tx: "now"}
})

Atomic transfer

proc transfer(from, to, amount) {
    let sender = find_one("accounts", {_id: from})
    if sender == null           { abort "sender not found" }
    if sender.balance < amount { abort "insufficient funds" }

    update("accounts", {_id: from}, {$inc: {balance: -amount, tx_count: 1}})
    update("accounts", {_id: to},   {$inc: {balance:  amount, tx_count: 1}})
    insert("ledger", {from: from, to: to, amount: amount})

    return {ok: true}
}

Three writes across two collections, one transaction. Either all happen or none.

Reads inside a transaction

Reads inside a procedure see a consistent snapshot. The version captured at find_one is what OCC validates against on commit.

proc reserve_seat(event_id, user_id) {
    let event = find_one("events", {_id: event_id})
    if event.seats_left <= 0 { abort "sold out" }
    update("events", {_id: event_id}, {$inc: {seats_left: -1}})
    insert("reservations", {event_id: event_id, user_id: user_id})
    return {ok: true, seats_left: event.seats_left - 1}
}

If two clients hit this at the same time, only one wins — the other gets an OCC retry-able error.

Multi-document atomic update

Different documents can be touched freely; only same-doc multiple updates are the problem.

proc batch_credit(account_ids, amount) {
    for id in account_ids {
        update("accounts", {_id: id}, {$inc: {balance: amount}})
    }
    return {ok: true, credited: amount}
}

When to NOT use OxiScript

  • Long-running batch jobs that touch millions of docs — break into smaller calls or use the pipeline.
  • Operations that need to commit partial results — OxiScript is all-or-nothing.
  • Workflows that wait on external systems mid-procedure — don't pause inside a transaction.
Report Issue