Using transactions for data integrityfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Imagine transferring money between two bank accounts: debit one, credit the other. If the server crashes between those two steps, you cannot be left with money debited from nowhere and credited nowhere — either both writes happen, or neither does. That all-or-nothing guarantee is exactly what a database transaction provides, and Drupal's Database API gives you a clean, object-oriented way to use one.

What you'll learn in this lesson

  • How to start a transaction with Connection::startTransaction()
  • Why Drupal's transactions are scope-based, not explicit commit/rollback calls
  • How an uncaught exception automatically triggers a rollback
  • Why nested transactions in Drupal are safe by design

Starting a transaction

function mymodule_transfer_funds($from_account, $to_account, $amount) {
  $connection = \Drupal::database();
  $transaction = $connection->startTransaction();

  try {
    $connection->update('accounts')
      ->expression('balance', 'balance - :amount', [':amount' => $amount])
      ->condition('id', $from_account)
      ->execute();

    $connection->update('accounts')
      ->expression('balance', 'balance + :amount', [':amount' => $amount])
      ->condition('id', $to_account)
      ->execute();
  }
  catch (\Exception $e) {
    // $transaction going out of scope on the next line rolls everything back.
    throw $e;
  }
}

Scope-based, not commit()/rollback()

This is the detail that surprises developers coming from other frameworks: there is no $transaction->commit() call anywhere above. Drupal's Transaction object commits automatically when it goes out of scope — when the function returns normally, PHP destroys the $transaction variable, and that destructor is what actually issues the COMMIT. You write ordinary PHP control flow; the transaction lifecycle rides along with it.

How rollback actually happens

If an exception is thrown and never caught before the function exits, PHP still destroys $transaction on its way out — but this time the destructor detects that an exception occurred and issues a ROLLBACK instead of a commit, undoing every write made since the transaction started. This is why the pattern above re-throws the exception rather than swallowing it: catching it silently, without re-throwing, would let the function return "normally" and commit half-finished work.

Nested transactions are safe in Drupal, unlike raw SQL: if a function that starts its own transaction calls another function that also starts one, most databases would silently commit the inner transaction the moment it goes out of scope — even though the outer one hasn't finished. Drupal's Transaction object tracks nesting depth internally and only issues the real SQL COMMIT when the outermost transaction in the call stack completes. You can nest freely without worrying about this.

Quick check: if you wrote try { ... } catch (\Exception $e) { /* do nothing */ } around the two updates instead of re-throwing, would the transaction still roll back on failure? (No — swallowing the exception lets the function return normally, so PHP destroys $transaction believing everything succeeded, and commits whatever partial writes did happen before the error. Re-throwing is what tells the transaction object a rollback is needed.)

Key takeaways

  • Start a transaction with \Drupal::database()->startTransaction() — there's no explicit begin/commit SQL to write yourself.
  • Drupal transactions commit automatically when the Transaction object goes out of scope, and roll back automatically if an exception is in flight when that happens.
  • Never swallow an exception you want to trigger a rollback — re-throw it (or otherwise let it propagate) so the transaction object can detect the failure.
  • Nested transactions are safe in Drupal — only the outermost one actually issues COMMIT/ROLLBACK at the database level.

Coming up next

You've now covered the full Database API: schema, reads, dynamic queries, static queries, and transactions. Next, the focus shifts from correctness to speed — starting with cache tags, the precision tool for invalidating exactly the cached content that depends on data you just changed.