The two concurrency primitives you actually used
CAS conditional updates and SKIP LOCKED claims — with both codebases' implementations side by side.
Know your own system
Strip away the vocabulary and your two projects control concurrency with exactly two primitives. Own them at the whiteboard level.
Primitive 1: the CAS update (compare-and-swap as SQL)
-- zoth: state machine transition UPDATE access_requests SET status = $3 -- or: status='ACTIVE', expires_at=$4 WHERE id = $1 AND status = $2 -- expected-value guard
RowsAffected == 1 → you won the transition. 0 → someone else changed it first → map to ErrConflict → 409. In zoth this is UpdateStatusConditional / ActivateConditional (internal/repositories/postgres/access_request.go) and the entire JIT state machine runs on it. Traverse equivalents: ring closure (updateMany where closedAt:null, count===1 is the claim), set-once topic (where topic IS NULL), the StreakDay upsert.
What it gives you: lock-free multi-replica safety (any number of processes can run the expiry loop; the row decides), no retry storms (losers get a clean semantic result), and an audit trail (each successful transition is a discrete event).
What it can't give you: multi-row invariants (Day 31's escalation ladder), and duration semantics — it's instantaneous, not a lease. Which is why you also need…
Primitive 2: the claim (lease via SKIP LOCKED)
-- zoth: outbox relay SELECT id FROM outbox_events WHERE published_at IS NULL ORDER BY created_at LIMIT $1 FOR UPDATE SKIP LOCKED; -- then: UPDATE outbox_events SET published_at = NOW() WHERE id = ANY($claimed) -- traverse: notification drain UPDATE notification_delivery nd SET attempts = attempts + 1, next_attempt_at = NOW() + INTERVAL '120 seconds' -- the lease FROM ( SELECT id FROM notification_delivery WHERE status='pending' AND next_attempt_at <= NOW() ORDER BY next_attempt_at LIMIT $n FOR UPDATE SKIP LOCKED ) claimed WHERE nd.id = claimed.id;
Two different flavors, one idea: the table row is the work item, the row lock (or a forward-dated lease) is the claim. Zoth's relay claims via transaction-scoped row locks (released on commit — if the process dies mid-batch, rows return to the pool automatically). Traverse's drain claims via a forward lease (next_attempt_at + 120 s) persisted in the row — a dead worker needs no cleanup; the lease expires and the next drain retries. Both patterns are production-grade Postgres work queues — and "Postgres SKIP LOCKED work queue" is a named topic in your study list; you've implemented it twice.
Why the lease version exists at all: the drain's work happens after the claim transaction commits (sending via APNs/FCM takes seconds and must not hold row locks). You can't hold a lock across external I/O — so the claim must be time-based. The forward lease means "I own this until 12:02; if I die, it's yours at 12:02." Reclaiming a stuck-but-alive worker's rows is impossible with pure leases (it will double-send) — bounded by idempotent sends? Push notification double-sends are acceptable (the coalescing and apns-collapse-id reduce the user-visible impact); if they weren't, you'd add fencing.
Where each appears (the full map)
| Need | Primitive | Site |
|---|---|---|
| request state transitions | CAS | zoth UpdateStatusConditional |
| expiry loop races | CAS (conflicts swallowed) | zoth expireOverdue |
| ring closure single-emit | CAS (count===1) | traverse rings |
| set-once AI topic | CAS (topic IS NULL) | traverse aiAnalysis |
| outbox publish claim | SKIP LOCKED row claim | zoth relay, 5 s |
| notification send claim | SKIP LOCKED + 120 s lease | traverse drain, 30 s |
| streak/ring day upserts | PK conflict handling | traverse gamification |
| consumer dedup | insert-on-conflict | both (Day 20) |
The narrative: "Between conditional updates and SKIP LOCKED claims, I've never needed a distributed lock, a lock service, or a leader — the database is the coordinator. That works because every unit of work is a row, and Postgres gives rows atomic transitions. The day work items stop being rows is the day I'd reach for ZooKeeper/Redlock — and I'd reach reluctantly."
Bulletproof your resume
Say these answers aloud before moving on
These are the follow-ups an interviewer will actually ask about your projects.
Why a forward lease instead of a 'sending' status in the notification drain?
A status needs cleanup: a crashed worker leaves rows stuck in 'sending' forever, requiring a reaper. A forward-dated lease IS the cleanup: next_attempt_at = now + 120s means a dead worker's rows become claimable again automatically, and a live-but-slow worker's rows just retry after backoff — the same code path as a first attempt. The tradeoff: a lease can double-send if the worker is alive but slow (its lease expired while it was still working) — acceptable for push (collapse-id coalesces), and bounded by MAX_ATTEMPTS=5.
Can your outbox relay and SKIP LOCKED claim lose events?
No — the claim transaction marks rows published only after the publish attempt ordering is deliberate: select+lock the batch, publish each, mark claimed IDs published, commit. If the process dies after publishing but before commit, the rows are still NULL → the relay republishes → at-least-once at the broker → the consumer's processed_events dedup absorbs the duplicate. So the relay is allowed to double-publish because the pipeline downstream is idempotent. Durability lives in the table, correctness lives in dedup — no exactly-once myth required.
What's the limitation of conditional updates I should respect?
Single-row invariants only. The guard is evaluated against one row's current version, so cross-row constraints ('at most 3 active grants per user', 'approve only if total exposure < X') need escalation: lock the shared parent row (SELECT FOR UPDATE the user), SERIALIZABLE with retry, or an aggregate trigger. I keep every transition single-row today, which is a design discipline as much as a database feature: when an invariant can be expressed as a row state, everything gets simpler.