reshadman/laravel-optimistic-locking
Add optimistic locking to your Laravel Eloquent models to prevent lost updates. It tracks version numbers on save, throws exceptions on conflicts, and works with your existing models—no schema changes beyond a version column.
Architecture fit. The package is a thin trait over Eloquent's persist lifecycle: it appends a WHERE version = ? predicate to updates and increments the counter, surfacing a stale-model exception when the affected-row count is zero. That is the right shape for Laravel: no new infrastructure, no service dependency, one integer column per protected table, and no changes to how models are declared beyond use OptimisticLocking. It fits read-modify-write domains (inventory, balances, order state, document edits) where last-write-wins is unacceptable. It does not fit append-only tables, low-contention models, or systems already serialized through queues.
Integration feasibility. Mechanically easy: composer require, add a version column via migration, add the trait, catch the exception at controller/job boundaries. Code churn is minimal. The real constraint is installability — the last release is January 2019, so Composer constraints are likely pinned to Laravel 5.x/6.x and PHP 7.x. On a modern Laravel stack this may not resolve without a fork or vendoring, which changes the effort from "adopt" to "own."
Technical risk. High relative to value. Single maintainer, 96 stars, no release in six years, no visible security process, and no guarantee of test coverage for edge paths. Framework drift is the dominant risk: Eloquent's save/update internals, saveOrFail, saveQuietly, soft deletes, and mass updates have all evolved since. The package also cannot protect writes that bypass Eloquent (increment, query-builder updates, raw SQL), so partial coverage can create false confidence. MIT licensing is favorable for a fork.
Key questions.
update, delete, force deletes, and soft deletes, or only save?Stack fit. Pure Illuminate/Eloquent with no external services; works on MySQL, Postgres, and SQLite. The only schema change is one integer column per protected table (default 0, backfilled). It composes naturally with existing model events, policies, and form requests because it does not alter the public model API beyond throwing on conflict.
Migration path. Add the version column, backfill to 0, add the trait to selected models, then introduce a single exception-handling convention at the HTTP and job boundaries (map stale conflicts to 409 or a bounded retry). Audit and either wrap or explicitly forbid non-Eloquent writes to protected tables. If the package will not resolve on our Laravel version, vendor it as an internal trait with our own tests rather than pinning the framework backward.
Compatibility. Treat this as a compatibility spike, not a given. Verify composer.json constraints against our Laravel/PHP matrix before committing. If incompatible, the fallback is a small internal trait implementing compare-and-swap on version (or on updated_at where a version column is undesirable), which is a few dozen lines and carries the same semantics with none of the abandonment risk.
Sequencing.
Maintenance. Effectively unmaintained upstream, so we own it. Budget for a fork or vendored copy with internal tests, and review it on every Laravel upgrade. The small surface area keeps this cheap, but it must be explicit ownership rather than "a dependency someone added."
Support. No vendor support channel; support is internal. The runbook is short — how conflicts are raised, how they are retried, and which writes are excluded — but it must exist, otherwise engineers will hit stale exceptions in production with no documented response.
Scaling. Optimistic locking generally improves throughput versus row-level pessimistic locks because it holds no locks during the read-modify-write window. The trade-off is contention: hot rows produce conflict storms and retry amplification. Mitigate with bounded retries plus jitter, queue serialization for the hottest aggregates, and a fallback to pessimistic locking or single-writer queues where conflict rates exceed a threshold. Track conflict rate per model as a first-class metric.
Failure modes.
increment, raw SQL, other services) → lost updates despite the protection.Ramp-up. Low for the mechanics — one trait, one column, one exception. Moderate for the concurrency discipline: teams must understand when to retry, when to surface a conflict to the user, and why bypassing the trait is unsafe. With a single maintainer and a small codebase, knowledge concentrates quickly; pair the rollout with a short internal guide and example handlers so the pattern does not depend on one engineer.
How can I help you explore Laravel packages today?