zaber-dev/laravel-lock
Application-level distributed locking for Laravel with entity-scoped locks, automatic owner detection, and cleanup. Use cache or database storage, attach locks to Eloquent models, and protect routes with middleware to prevent race conditions.
Feature-level decisions
Lock::for('billing_charge', $user)), which is the missing primitive behind most "user got charged twice" incidents.->middleware('lock:billing_charge,30') lets you declare concurrency policy in the routing table, with automatic 429 and finally-based release. This makes "this endpoint is single-flight per user" reviewable in a diff rather than buried in a controller.LockAcquired/LockReleased/LockFailed events let you alert on contention. That's how you find out a lock is over-broad before it becomes a throughput problem, and it converts "should we invest in concurrency work?" into a data question.Roadmap decisions
Cache::lock() plus ~100–300 lines of internal glue in every service that needs it, (b) this package, (c) a dedicated distributed-lock service (Redis/etcd/ZooKeeper with fencing). This package is the "buy" option for teams that want the ergonomics without operating new infrastructure.locks() polymorphic relation (good where "who held this and when" matters for support or compliance). That's a genuine product knob: do we need lock records to survive a cache flush?user_id, job ID, request hash) means "a worker only releases what it owns." That's a correctness decision that most hand-rolled implementations get wrong, and it's worth standardizing centrally rather than re-deriving per team.Use cases that map cleanly: duplicate payment/refund processing, inventory decrement at checkout, webhook processing idempotency guards, financial transfer coordination, single-flight cron, admin "don't let two support agents act on the same ticket" guards.
Adopt when most of these are true:
Cache::lock() calls with ad-hoc key naming.locks table.429 behavior across services rather than per-team implementations.Prunable + polymorphic locks() relation) for support debugging or audit.HasLocks, facade, events) over a bespoke internal utility.Look elsewhere / do additional diligence when:
Cache::lock() is free, battle-tested, and already in the framework. A dependency isn't justified for two call sites.model:prune scheduling, cache-store durability settings, alerting on LockFailed.Recommended diligence gates before adoption: a staging load test on the hottest endpoint, a documented decision on driver-per-use-case, a written statement of what fencing you rely on for money movement, and a review of what happens on cache flush/deploy.
To executives (1–2 minutes): "We lose money and trust when two copies of the same operation run at once — double charges, oversold inventory, duplicate webhooks. Today each team solves that differently, or doesn't. This is a small, MIT-licensed Laravel library that standardizes it in a few lines: 'only one of these may run at a time for this user/order,' with automatic cleanup and audit events. Integration is roughly a day of work, no new infrastructure if we already run Redis, and it removes an entire class of incident rather than one bug. It's a small open-source dependency from a single maintainer, so we'll isolate it behind our own thin interface — that keeps the ergonomics while making it a two-hour swap if we ever need to. Ask: approve a one-sprint pilot on the billing endpoint, success measured by duplicate-charge incidents and lock-contention alerts."
To engineering leadership:
"It's the missing middle layer between Cache::lock() and a bespoke locking service. Same primitive underneath; the value is owner detection (only the holder releases), Eloquent scoping, middleware with correct finally semantics, swappable cache/database stores, and typed lifecycle events for monitoring. Realistic cost: half a day to integrate and configure, plus decisions on driver-per-use-case and prune scheduling. Main risk isn't the code — it's treating any TTL lock as a correctness guarantee. We should pair it with idempotency keys on anything touching money, and keep an internal LockManager-shaped interface so the vendor dependency stays shallow. If we only ever need one lock site, use the framework primitive instead; the case for this package is standardization across several."
To the team (practical framing):
"Use Lock::for(...)->block(...) for burst-prone work, the middleware for endpoint-level single-flight, and the database driver only where we need to answer 'who held this and when.' Watch the LockFailed event volume — rising contention means a lock is too broad, not that we need more servers."
How can I help you explore Laravel packages today?