Laravel interview question · Most asked

How do database transactions work in Laravel, and what pitfalls should you know?

Summary

DB::transaction(fn () => ...) commits if the closure returns and rolls back if it throws. It accepts a retry count for deadlocks. Side effects such as dispatching jobs or sending mail should wait until after the commit.

Longer Answer

DB::transaction($callback, attempts: 3) retries the whole closure when a deadlock occurs. Manual control is available with beginTransaction, commit and rollBack. Nested calls use savepoints.

Pitfalls:

  • Jobs and events dispatched inside a transaction may run before the commit and not find the data (or find it rolled back). Use ->afterCommit() on the job or listener, or set after_commit on the queue connection; DB::afterCommit(fn () => ...) does the same for arbitrary code.
  • External calls (HTTP, email) inside a transaction keep locks open and cannot be rolled back. Do them after the commit.
  • Transactions do not prevent race conditions on their own: for read-then-write logic use lockForUpdate() (or sharedLock()) inside the transaction, or an atomic conditional update.
  • Exceptions caught inside the closure do not roll back.

Code Example

DB::transaction(function () use ($orderId) {
    $order = Order::whereKey($orderId)->lockForUpdate()->firstOrFail();
    $order->markPaid();
    ShipOrder::dispatch($order)->afterCommit();
}, attempts: 3);

Key Points

  • Closure form commits or rolls back automatically; retries on deadlock.
  • Dispatch jobs afterCommit.
  • No external I/O inside transactions.
  • Use lockForUpdate() for read-modify-write.

Deep dive: Transactions, Locking & Concurrency in Code

Explanation

What interviewers look for

Awareness of the after-commit problem with queued jobs. It is a classic production bug.

Follow-up questions

  • What isolation level does MySQL use by default?
  • How would you prevent two workers from processing the same order?

More most-asked Laravel questions

← All Laravel questions