SupportWriter space ↗
← Back to the journal
Laravel

Handle Edit Conflicts in Laravel with Optimistic Locking

Prevent user edits from being overwritten through the version, conditional update, and HTTP 409 response columns.

DAdministrator · 13 Oct 2026 · 2 min read

You are reading a translated version.

Two editors open the same document. The first editor saves the fixes, then the second editor saves the old tabs. Calling save() ordinary can override the first repair without warning. Short transactions at the time of storage do not know that the second form is from the old version.

The solution is optimistic locking: the form carries the read version number, and the update only works if the database version is still the same. The following case studies use Laravel 13 and PHP 8.5. This pattern is suitable for documents that rarely experience edit collisions; users get the opportunity to combine changes when a collision occurs.

Add version number

Create a migration with php artisan make:migration add_version_to_documents_table --no-interaction. In the method up(), add the following columns to an existing table:

Schema::table('documents', function (Blueprint $table): void {
    $table->unsignedInteger('version')->default(1);
});

Methods down() deletes that column. The example assumes the table also has title, body, and timestamps. The edit form sends hidden inputs version of the document when opened. Validation version as required|integer|min:1; validation of title and content according to application limits. Hidden inputs remain untrusted inputs and do not replace authorization.

Update with version conditions

Here's the controller part; Document is the application model and UpdateDocumentRequest is the Request Form that has been created. Implement the policy update, then call before changing the data.

<?php

namespace App\Http\Controllers;

use App\Http\Requests\UpdateDocumentRequest;
use App\Models\Document;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;

class DocumentController extends Controller
{
    public function update(UpdateDocumentRequest $request, Document $document): JsonResponse
    {
        Gate::authorize('update', $document);
        $data = $request->validated();
        $expectedVersion = (int) $data['version'];

        $updatedRows = DB::table('documents')
            ->where('id', $document->id)
            ->where('version', $expectedVersion)
            ->update([
                'title' => $data['title'],
                'body' => $data['body'],
                'version' => DB::raw('version + 1'),
                'updated_at' => now(),
            ]);

        if ($updatedRows === 0) {
            return response()->json([
                'message' => 'Dokumen berubah. Muat versi terbaru sebelum menyimpan.',
            ], 409);
        }

        return response()->json(['version' => $expectedVersion + 1]);
    }
}

Versioning and upgrading requirements take place on a single SQL statement. Don't read the version, compare it in PHP, then update the version unconditionally: other changes can fit between the two steps. The upgrade also ensures that the number of lines changes remains one even though the title and content are identical.

Displays conflicts without losing a job

When receiving a 409, the interface stores the text that is being typed in memory or local storage as needed for privacy. Offer load server version and comparison view. Do not automatically repeat the request with the latest version number because it overwrites other editors' work.

Sample response returns only the version number. To send the most recent server content, reread it through an authorizing endpoint; the response version may change again before the user presses save. For deleted documents, the application can distinguish 404s through additional queries, but generic 409s can also be used according to API contracts.

All authors must follow the same contract

Optimistic locking fails if the admin dashboard, import job, or maintenance command changes the content without upgrading. Centralize relevant writing paths in the service. The Query Builder does not call an Eloquent model event; if the application relies on the observer for an audit, write the audit explicitly in the same transaction as the conditional update.

If document permits are subject to change along with storage and permit changes must be atomic, include relevant ownership requirements or conduct permit checks in transactions with appropriate locking strategies. The document version number does not automatically lock the team membership.

Skenario Pengujian

Read version one to two of the form. The first save must be successful and result in version two. The storage of the second form with version one must get 409 and not change the contents. Add unauthorized user cases, missing versions, and updates via job. Use assertion against the contents of the database, not just the response status.

API Reference: Update statements and Authorization via gates.

Comments 0

Share your questions, experiences, or feedback about this article.

No comments yet. Be the first to share your feedback.

← Explore more notes