# Reservation expiration safety

**Purpose:** Correct, **idempotent**, **race-safe** expiration.  
**Principles:** [`reservation-domain-principles.md`](reservation-domain-principles.md) Part 5.

---

## Part 4 — Expiration correctness rules

### Expiration idempotency

- Each expiration attempt must be **keyed** (`reservation_id`, `sweep_batch_id` or monotonic `expire_entry_id`) so **re-sweep** does not double-reduce `reserved`.
- Preferred: append **`expire` ledger row** only if preconditions still hold; projection update is **pure function** of ledger replay.

### Sweep concurrency safety

- Process reservations in **lock order** `(tenant_id, warehouse_id, item_id, reservation_id)` ascending.
- For each candidate: `SELECT … FOR UPDATE` **projection row** → re-read reservation open qty → if still expirable, write `expire` + update projection in **one transaction**.

### Expiration replay safety

- Rebuild jobs replay `expire` like other entries; replay must reproduce same `reserved` totals as incremental path ([`replay-consistency-rules.md`](replay-consistency-rules.md)).

### Expiration race-condition handling

| Race | Rule |
|------|------|
| Sweep vs `consume` / `stock_out` | **Fulfillment wins** if it commits first; sweeper sees `open_qty = 0` and skips. |
| Double sweep worker | Idempotent `expire` or dedupe table for `(reservation_id, reason=expire)` per batch. |
| Clock skew | Use **DB `now()`** inside transaction for `expires_at` comparison; document client clock not authoritative. |

### Auditability

- Every successful `expire` records **who/what** (job id, rule version) in `metadata` ([`reservation-domain-principles.md`](reservation-domain-principles.md)).
