Transactional operations in the database in Laravel

Video thumbnail

Depending on the type of application we want to build, it is often necessary to perform multiple database operations securely, and this doesn't only apply to many-to-many relationships. For example, in the case of buying a book, the steps could be as follows:

  1. Register the payment identifier in the database.
  2. Deduct quantities from the inventory.
  3. Register the product in some of the customer's purchase lists.

These are just a few possible steps; there may be more, and they do not necessarily have to be executed in a specific order. There are many scenarios where several consecutive operations are required, and if any of them fail, all previous operations should be reversed to avoid leaving the process in an intermediate state.

What are transactional operations in Laravel?

Transactional operations allow you to group several database queries into a single unit of work. Either all of them execute successfully or none of them do.

In practical terms:

  • If everything goes well → commit
  • If something fails → rollback

This guarantees the ACID properties:

  • Atomicity: all or nothing.
  • Consistency: the database never remains in an invalid state.
  • Isolation: other operations do not see intermediate states.
  • Durability: once confirmed, the change persists.

The real problem with multiple operations

A very common example (and one I have experienced several times) is a purchase:

  • Register the payment.
  • Deduct stock.
  • Associate the product with the user.

If the stock fails in step 2 and the payment has already been registered, the order enters an intermediate state. That “limbo” is exactly what transactions avoid.

For example, if during step two we discover that there is no more stock available, the operation would fail. Consequently, step three could not be completed, leaving the order in limbo, since step one would have been executed correctly.

Why transactions are key in real applications

These allow grouping a set of queries into a single unit of work, ensuring that all of them execute correctly or none of them do.

In other words:

  • If a query fails, all previously made modifications to the database are rolled back.
  • This guarantees data integrity, preventing an order from being left incomplete or in an inconsistent state.
  • Atomicity of operations is ensured, as all are executed as a single unit of work.

In our book purchase example, if a problem occurs in any of the steps, Laravel will automatically roll back all operations. This way, the application remains safe and reliable, avoiding problems such as inventory inconsistencies, duplicate records, or incomplete transactions.

Without transactions:

  • Incomplete orders
  • Negative inventories
  • Orphan records
  • Impossible-to-debug states

With transactions:

  • The system protects itself.
  • The code is more predictable.
  • Errors are controllable.

How to implement transactional operations in Laravel

Laravel offers two main ways to work with transactions.

Using DB::transaction() (recommended way)

This is the cleanest option when operations are clear and you don't need extra logic in the rollback.

use Illuminate\Support\Facades\DB;

try {
    // inicia la transaccion
    DB::beginTransaction();

    // Realiza tus consultas aquí
    DB::table('users')->insert(['name' => 'John Doe']);
    DB::table('orders')->insert(['user_id' => 1, 'total_amount' => 100]);

    DB::commit(); // Confirma los cambios en la base de datos
} catch (\Exception $e) {
    DB::rollback(); // Revierte los cambios en caso de error
    return $e->getMessage();
}

In this example:

  • DB::beginTransaction() initializes the transaction.
  • Inside the try block, we perform all our database operations through our models or similar.
  • If all the operations we wanted to perform were resolved successfully, we call the DB::commit() method to confirm the changes.
  • If an error occurs, the catch block calls DB::rollback() to revert all changes that may have been made.

It can also be implemented using a callback:

DB::transaction(function () {
    // Perform your queries here
    DB::table('users')->insert(['name' => 'John Doe']);
    DB::table('orders')->insert(['user_id' => 1, 'total_amount' => 100]);
    DB::commit();
});

Another example:

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $order = Order::create([
        'user_id' => auth()->id(),
        'total' => 100
    ]);

    foreach ($items as $item) {
        $product = Product::findOrFail($item['id']);
        $product->decrement('stock', $item['quantity']);

        if ($product->stock < 0) {
            throw new Exception('Stock insuficiente');
        }

        $order->items()->create([
            'product_id' => $product->id,
            'quantity' => $item['quantity']
        ]);
    }
});

If any exception occurs:

  • Laravel rolls back automatically.
  • You don't need manual try/catch.

Manual handling with beginTransaction, commit, and rollback

When I need total control (logs, custom error handling, or notifications), I prefer this option.

use Illuminate\Support\Facades\DB;
DB::beginTransaction();
try {
   $user = User::create($data);
   $user->syncRoles([$data['role']]);
   // otras operaciones críticas...
   DB::commit();
   return $user;
} catch (\Exception $e) {
   DB::rollback();
   throw $e;
}

This approach has been useful for me when I need to log specific errors before rolling back changes.

Transactions using Eloquent models

A key advantage is that transactions work perfectly with Eloquent relationships:

DB::transaction(function () {
   $user = User::create([
       'name' => 'John Doe',
       'email' => 'john@example.com',
   ]);
   $user->profile()->create([
       'bio' => 'Perfil creado dentro de la transacción',
   ]);
});

If the profile fails, the user is not saved.

Common use cases for transactional operations

Creation of orders and products

  • Order
  • Details
  • Inventory deduction

Payments and inventory

On more than one occasion, I detected negative stock only after correctly implementing transactions. Before, the problem existed, it just wasn't visible.

Users, roles, and permissions

Creating a user + assigning roles + sending a notification is a classic flow that must be atomic.

Common mistakes when using transactions in Laravel

Sending emails or events inside the transaction

One of the most common mistakes I've seen:

  • ❌ Sending emails inside the transaction
  • ✅ Triggering events after the commit

Transactions do not control external systems.

Database engines without support

Not all engines support transactions:

✅ InnoDB

❌ MyISAM

This is key: if the engine doesn't support them, the code won't fail… but it won't work as you expect either.

Best practices and advanced considerations

Nested transactions

Laravel supports them, but the outer transaction is the one in charge:

DB::transaction(function () { DB::transaction(function () { // inner transaction }); });
DB::transaction(function () {
   DB::transaction(function () {
       // transacción interna
   });
});

Performance and long queries

A transaction open for a long time:

  • Locks records
  • Reduces concurrency

My rule of thumb: short and precise transactions.

Testing with transactions

In automated tests, transactions are pure gold:

use Illuminate\Foundation\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
   use DatabaseTransactions;
}

Each test rolls back changes automatically.

Conclusion

Transactional operations in Laravel are not an extra: they are a necessity when working with real processes. Every time several operations depend on each other, a transaction is the difference between a reliable system and a fragile one.

Since I incorporated them consistently, bugs due to inconsistent data practically disappeared.

We have already learned how to perform many database operations, but it is best to do it optimally, learning how to perform queries with Eloquent efficiently.

We will learn how to use Transactional Operations in the database with Laravel so that we can perform several operations on the same task unit in Laravel.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español