Transactions

Every request you send to ReifyDB runs inside a transaction, automatically. Writes commit atomically with serializable snapshot isolation, reads see a consistent snapshot, and conflicts are retried for you. There is no BEGIN, COMMIT, or ROLLBACK to manage - transaction handling is the engine's job, not yours.

Queries, commands, and admin

ReifyDB splits work into three kinds of transactions, and the clients expose them as three entry points:

  • --A query transaction (query) only reads. It runs under snapshot isolation: it pins the latest committed state when it starts and sees exactly that state for its entire run - never a half-applied write, no matter how many writers are active.
  • --A command transaction (command) writes data: inserts, updates, deletes, procedure calls. It runs under serializable snapshot isolation - the engine tracks what each command reads and writes, and only lets it commit if the result is equivalent to the commands running one after another. A command cannot change schema.
  • --An admin transaction (admin) is the only kind that can execute DDL - create, alter, drop, migrations, access control. It can also write data, so a schema change and the writes that go with it commit as one atomic unit.

Keeping schema changes out of command transactions means day-to-day application traffic physically cannot alter the schema - evolving it is a deliberate, separately privileged operation. There is no isolation level to configure and no weaker mode to fall back to.

One request, one transaction

Each request is exactly one transaction. A request may contain many statements - they all commit together, or none of them do. This transfer touches two rows in two separate update statements, and no reader can ever observe the state between them (the runnable snippets on this page execute as admin requests, which is why they can mix create statements with writes):

$ A Multi-Statement Command Commits Atomically
$ ctrl+enter to run

If any statement fails, the whole request rolls back. Here the update executes first and the cast fails afterwards:

$ An Error Rolls Back the Whole Request
$ ctrl+enter to run

The update was rolled back with everything else - the balances are untouched:

$ Nothing Was Applied
$ ctrl+enter to run
No BEGIN, no COMMIT - by design
ReifyDB has no statement to hold a transaction open across requests. Frontends talk to the database directly, and an open transaction owned by a browser tab is a liability: it pins resources until a client that may have disappeared decides to finish it. Bundling each request into one atomic transaction removes that failure mode and is what makes automatic conflict retry safe - nothing is committed until the whole request succeeds. The rollback keyword you may encounter in migrations is unrelated: it declares compensating statements for reverting a migration, not transaction control.

Readers never block writers

ReifyDB uses multi-version concurrency control (MVCC). Every commit produces a new version of the rows it touched instead of overwriting them in place, and every committed transaction is stamped with a monotonically increasing commit version. A query simply reads as of the newest committed version at the moment it starts. It takes no locks, blocks no command, and is blocked by none - long analytical reads and high-frequency writes coexist without queueing on each other.

Conflicts and automatic retries

Commands are optimistic: they do not lock rows up front. Instead, each command works against its own snapshot, and at commit the engine validates that no other transaction has committed a change that overlaps with what this command read or wrote. If validation fails, the command aborts with a conflict error (TXN_001) - and the server retries it automatically with backoff, up to 10 attempts by default. Each retry re-executes the request from scratch against the newest state, so the retried command sees the data that beat it to the commit.

In practice you rarely see conflicts at all: queries never conflict with anything, and commands only conflict when they genuinely race over the same data. If a command still fails with TXN_001 after all retries, the client receives the error and can decide whether to resubmit.

Views, handlers, and transactions

Derived state participates in the same guarantees. Transactional views are maintained inside the commit of the write that affects them - the table and the view change in the same atomic step:

$ A Transactional View over a Table
$ ctrl+enter to run

Write to the source table:

$ Write to the Source Table
$ ctrl+enter to run

The view is already current - there is no window in which the table shows the new order but the view shows the old revenue:

$ The View Committed with the Write
$ ctrl+enter to run

That timing has one consequence worth knowing: once a request writes to a view's source data, reading that view later in the same request is an error (TXN_015), and the whole request fails and rolls back. Mid-request the view still holds its pre-request contents, and ReifyDB fails loudly rather than hand you stale data:

$ Write Then Read the View in One Request
$ ctrl+enter to run

The failed request committed nothing, and reading the view in its own request works as before:

$ The Failed Request Left Nothing Behind
$ ctrl+enter to run

The rule applies to transactional and deferred views alike, and it follows view chains - a view built on top of another view is protected too. Reading a view before writing to its sources is fine. The remedy depends on the view kind: a transactional view is current the moment the write returns, so split the write and the read into separate requests, or read the source tables directly. A deferred view updates asynchronously after commit, so read the source tables directly, or consume the view through a subscription:

$ A Deferred View over the Same Table
$ ctrl+enter to run

Writing upstream of the deferred view and reading it in the same request fails the same way:

$ Deferred Views Are Protected Too
$ ctrl+enter to run

Handlers run synchronously inside the writing transaction too - if a handler fails, the write that triggered it rolls back. Deferred views and subscriptions are the asynchronous side: they consume committed changes after the fact, ordered by commit version, and are eventually consistent.

Under the hood

A command buffers its writes locally and records the keys and ranges it reads. At commit, a central coordinator checks that read/write set against every transaction that committed since the command's snapshot; if nothing overlaps, it allocates the next commit version and publishes the write-set - to storage, to transactional view maintenance, and to the change log that feeds subscriptions, all under that one version. For the full mechanics - the commit pipeline, conflict windows, and watermarks - see Transaction Internals.

Commit and the disk
Committed data is immediately visible to every subsequent transaction, and is persisted to disk asynchronously. What that means for crash recovery is covered in Durability & Storage.