← Laravel: requests, data and application decisions
06 / 10 · 65 MIN

Transactions, concurrency and migrations

Bound a transaction by connection and engine, protect the read used in a concurrent decision, and distinguish schema migration, data recovery and release compatibility.

The closure and the connection boundary

In DB::transaction, an exception leaving the closure causes rollback and is rethrown; success leads to commit. This description assumes transactional operations covered by the connection and does not remove engine-specific contracts. If orders has a transaction and audit independently writes through another connection, rolling back orders does not establish rollback of audit.

Check the effective connection of queries and models. Matching credentials or similar names do not turn two connections into one transaction.

Protect the read that makes the decision

Two reads of stock=1 outside a transaction can produce two positive decisions even when each later write is transactional. On an engine with transactional row locks, read with lockForUpdate inside the transaction that checks and changes availability.

All competitors must follow the same protocol. Acquiring a lock late while still using the old value does not refresh the decision. Uniqueness also has a write boundary: two pre-validations may pass; an appropriate UNIQUE constraint arbitrates concurrent inserts and requires conflict handling.

Deadlock retries and external effects

The transaction attempt argument permits retrying the closure after deadlocks. If an attempt called an HTTP service before failing, that service may already have accepted the action. Database rollback does not undo that acceptance, and retry can duplicate it.

Separate local persistence from external delivery and define appropriate operation identity, confirmation and retry behavior. Acceptance analysis should identify effects left pending or confirmed; increasing attempts without that contract simply repeats the uncertainty.

DDL and implicit commits

Do not automatically extend the transactional-write contract to every SQL statement. Documentation identifies CREATE TABLE in MySQL as an example that can cause implicit commit when executed through statement or unprepared. Laravel may become unaware of the engine’s actual transaction level.

If a later operation fails, a transaction closure alone does not justify concluding that everything returned to its initial state. Identify statements and engine, and establish effective state before planning recovery.

Define columns, keys and modifier order

A migration describes schema changes in up and their reversal in down. Calls must express the intended contract: optional reviewer_id referencing users requires nullable before constrained and an explicit users target. A default zero is not equivalent to null and does not prove a user with that ID exists.

Unique indexes and foreign keys impose different constraints; application validation helps explain invalid input but does not replace these controls over concurrent writes or other clients.

Reverse structure and recover data

Dropping a populated column and recreating it empty in down restores a schema shape, not the removed history. Define how values will be preserved or recovered before accepting a destructive change. Also consider old processes still running: web processes and workers can coexist and continue reading the column.

Defer removal until that dependency ends. Migration rollback operates on batches, which may contain several files; choose the intended scope and reconcile functional results instead of treating down’s presence as proof of complete reversibility.

Isolated execution and evidence of application

migrate --isolated uses an atomic cache lock to prevent concurrent command execution. Distributed instances must share the central cache service supporting the lock; the same key in independent local caches does not coordinate them. An instance finding the lock occupied can skip migrations and still exit successfully.

Inspect migration state and confirm the actual executor’s result. The lock coordinates execution but does not demonstrate compatibility of application versions with the new schema.

<?php
// Decision excerpt; imports, schema and request authorization are omitted.
// Assumes a transactional engine with row locks and the same protocol for all writers.
$db = DB::connection('orders');
$db->transaction(function () use ($db, $itemId) {
    $stock = $db->table('stock')->where('id', $itemId)
        ->lockForUpdate()->first();
    if ($stock === null || $stock->available < 1) {
        throw new RuntimeException('No stock available');
    }
    $db->table('stock')->where('id', $itemId)
        ->update(['available' => $stock->available - 1]);
});
// A complete reservation also needs identity, persistence and retry contracts.

// Separate migration fragment: reviewer refers explicitly to users.
$table->foreignId('reviewer_id')->nullable()->constrained('users');
IN PRACTICE

The decision uses the value read under a lock inside the same transaction that changes it, on the orders connection. It does not reuse stock observed before the transaction. The excerpt assumes an engine with transactional row locks and writers following the protocol; it does not claim that guarantee for SQLite.

Complete reservation persistence, retry identity, imports and schema are not implemented. The separate migration fragment applies nullable before the explicit foreign key to users. The activity is interpretation without execution.

Common pitfalls

Reading stock before the transaction; assuming pre-validation guarantees uniqueness; repeating external calls inside a closure without a retry contract; expecting rollback of MySQL DDL; confusing column recreation with recovery of its values; using separate local caches to coordinate instances.

Related topics: Eloquent, relationships and query cost · Queues, retries and scheduling · Tests, cache and release diagnosis

Take this idea with you

The guarantee depends on the protected interval, connection, engine and effects involved. Migration success must be reconciled with effective schema and active consumers. Recovery includes values and behavior alongside the existence of a down method.

Create account

Reference: Database transaction closure · Laravel 13.x; PHP 8.3+