padosoft-atomic-invariantslisted
Install: claude install-skill padosoft/skills
# Atomic invariants
**Either the invariant is recorded in the same atomic step that checked it, or the invariant does not exist.**
Everything else — a check a few lines above the write, a lock released before the update, a "nobody clicks
twice" assumption — is a race that shows up under exactly the conditions you care about: a retry, a double
tap, a load spike, two workers on the same queue.
---
## 1. The shape of the bug
```php
// ❌ two requests both read "not used yet" and both proceed
$coupon = Coupon::whereCode($code)->firstOrFail();
abort_if($coupon->used_at !== null, 409);
$coupon->update(['used_at' => now()]);
```
The gap between the read and the write is where the second request fits. It does not need to be wide: two
requests arriving in the same millisecond is the normal case for a double tap on a phone, a retried mobile
request, or two queue workers.
```php
// ✅ read and write in the same transaction, with the row locked
DB::transaction(function () use ($code) {
$coupon = Coupon::whereCode($code)->lockForUpdate()->firstOrFail();
abort_if($coupon->used_at !== null, 409);
$coupon->update(['used_at' => now()]);
});
```
**The lock is held until the invariant is recorded.** A `lockForUpdate()` whose `update()` happens after the
closure returns bought nothing at all.
## 2. Back it with a constraint where the rule demands it
A transaction is the fast path. A **unique constraint** is the one that is still true under a deploy, a
replica that lags, a sec