# Padosoft Atomic Invariants

> Use this skill whenever code enforces something that must hold only once or only so many times — a single-use code or coupon, a nonce, a rate or quota limit, "the first one wins", a stock reservation, an idempotency key, a state transition that must happen once — and whenever the user reports that two requests both succeeded where one should have failed, a coupon was redeemed twice, a job ran twice, or a counter drifted under load. It checks that the read and the write of the invariant live in one atomic step and that a constraint backs the rule where it matters. Do not use it for general transaction design, for retry policy, or for distributed consensus.

- Skill: `padosoft/padosoft-atomic-invariants` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add padosoft/padosoft-atomic-invariants`
- Raw SKILL.md: https://api.skillmd.com/api/skills/padosoft/padosoft-atomic-invariants/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: padosoft (https://skillmd.com/u/padosoft)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/padosoft/padosoft-atomic-invariants

---


# 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 second service that writes the same table, or a code path someone adds next year and
forgets to wrap.

```sql
ALTER TABLE coupon_redemptions ADD UNIQUE KEY uq_coupon_once (coupon_id);
```

Then treat the constraint violation as a normal outcome, not as a 500: catch it and return the same answer
the check would have returned. **A constraint you never expect to fire is a constraint you never handled.**

The rule of thumb: if two rows would be a *business* problem, the database says so. If they would only be
untidy, the transaction is enough.

## 3. Check the write actually happened

```php
$affected = Coupon::whereCode($code)->whereNull('used_at')->update(['used_at' => now()]);
if ($affected === 0) {
    // somebody else got there first — this is the answer, not an error
}
```

A conditional update that returns its affected-row count is often simpler than a lock, and it is atomic by
construction: the condition and the write are one statement. **Then read the count.** Ignoring it turns the
whole pattern back into a check-then-act.

Same for ownership checks inside a transaction: verify, write, and confirm the row count before answering
success.

## 4. Idempotency is the same rule, seen from the caller

Mobile networks retry. Queues retry. Users double tap. So a mutating operation that must happen once takes an
**idempotency key generated by the caller at the moment of intent**, and the server records that key in the
same transaction as the effect. The second request with the same key returns the first result instead of
doing the work again.

A key generated server-side per request is not an idempotency key — it is a request id.

## 5. Where the check must not be

- **Not in the client.** A disabled button, a guard in the UI, a confirmation dialog: those are user
  experience. The invariant is enforced where the write happens.
- **Not in a cache.** A "have we seen this already" lookup in a cache without a durable record is a hint, not
  a control, and it fails open on a restart or an eviction.
- **Not in a separate service call** whose result is used a few lines later — that is a check-then-act with a
  network hop in the gap.

## 6. When failing open is the right call — and when it is not

A **rate limiter** that fails open when its store is unavailable is a deliberate and usually correct choice:
rate limiting is an availability control, and failing it closed turns a cache blip into a full outage.
Authorization sits upstream and does not depend on it.

A **single-use invariant** that fails open is a defect, always: it means the thing that must happen once can
happen twice.

Write down which of the two you are building, next to the code. The difference is not visible from the
implementation, and the next person will assume the wrong one.

---

## How to find it in a diff

```bash
# a check followed by an update, no transaction in sight
rg -n -B2 -A6 "(firstOrFail|first\(\)|find\()" app/ src/ | rg -n "update\(|save\(" | head
# locks whose transaction boundary is unclear
rg -n "lockForUpdate|FOR UPDATE|SELECT .* FOR SHARE" app/ src/
# update() whose return value is discarded
rg -n "^\s*(\\\$\w+->)?update\(" app/ | rg -v "^\s*\\\$\w+\s*="
# single-use wording without a constraint nearby
rg -n -i "used_at|redeemed|consumed|one[_-]?time|nonce|single[_-]?use" app/ src/ database/
```

## Gotchas

- **A transaction that spans a network call holds locks for the duration of that call.** Do the external work
  outside; keep the invariant's read and write inside.
- **The framework's "unique" validation rule is not a constraint.** It is a query before the insert, which is
  exactly the race this skill is about.
- **Two workers is not a hypothetical.** Any queue with concurrency above one, any horizontally scaled app,
  any retry — the second execution is the normal case, not the edge.
- **A test cannot usually reproduce this alone.** Test the pieces: assert the constraint exists, assert the
  conditional update returns zero on the second call, assert the violation is handled as a business outcome.
- **"It has never happened" is a statement about traffic, not about correctness.** It starts happening on the
  day the campaign goes out.

## Checklist

- [ ] The read of the invariant and the write that records it are in one atomic step
- [ ] The lock (if any) is held until after the write, inside the same transaction
- [ ] A unique constraint backs the rule where two rows would be a business problem
- [ ] The constraint violation is handled as an outcome, not as a 500
- [ ] Affected-row counts are read, not discarded
- [ ] Idempotency key generated by the caller, recorded with the effect
- [ ] Fail-open vs fail-closed is a written decision, next to the code

## Final report

```
Invariant: <what must hold once>
Enforced: <transaction + lock | conditional update + row count | unique constraint>
Constraint present: yes <name> | no, because <reason>
Violation path: <what the caller gets>
Idempotency: <key source, where recorded> | not applicable
Fail mode under store outage: open | closed — and why
```

