A conditional claim UPDATE gives an outbox row one current owner when two workers race for it on the same database.
Spring outbox claim race: one conditional UPDATE wins
The local race
ConcurrentOutboxLeaseTest opens a separate H2 connection for each worker through DriverManagerDataSource. Two threads wait on a start latch, then issue the same guarded UPDATE for EVENT-81 with different tokens. Exactly one UPDATE reports one changed row; the other reports zero. The row stores the winner token. The test has a finite wait for worker readiness and results, so a stalled thread fails the test rather than hanging it.
This covers one H2 row and one two-thread race. H2 may lock and isolate differently from PostgreSQL, MySQL or another target database. The assertion is a contract to rerun in the deployment database, not evidence that every engine has identical throughput or contention behavior. The lease lesson explains expiry; the batch lesson explains why a preceding SELECT does not grant ownership.
Protect the claim after selection
Both workers may read the event ID before either changes the row. The winner is decided by the UPDATE predicate, not by which thread ran SELECT first. Guard state and expiry in the mutation itself. If no row changes, the worker must not publish that event. Do not fabricate a successful claim by using the selected row as proof.
At scale, measure lock waits, deadlocks and selection plans with due rows and multiple processes. Batching with SKIP LOCKED can improve contention on some engines but changes fairness and syntax; the checked H2 SQL uses no such feature. The integration plan names the target-database checks still required.
Checked source
update lease_event set claim_token = ?, lease_until = ?
where event_id = 'EVENT-81' and state = 'PENDING'
and (claim_token is null or lease_until <= ?);Verification boundary
ConcurrentOutboxLeaseTest.twoConcurrentConnectionsYieldOneCurrentClaim in the downloadable Spring source kit. The excerpt is shortened; the kit contains the complete checked SQL.
Costs and limits
The test uses two H2 connections and one row. It does not run two service processes, a broker, target-database isolation variants, or a sustained load test.
Common Mistakes
- Do not publish an event after a zero-row claim.
- Do not treat a SELECT as a lock unless the database query explicitly guarantees it.
- Do not generalize an H2 winner test to another engine without rerunning it.
Read next
Spring outbox claims: lease expiry and token-fenced acknowledgement, Spring outbox polling: cap work per tick and measure backlog age, Spring relay integration tests: prove the boundaries H2 cannot, Spring outbox transaction boundaries: where atomicity ends.
