An outbox acknowledgement should update a row only when the caller still holds its current claim token.
Outbox acknowledgements: require the current lease token
A stale owner must lose
Worker-A claims E-41. After the logical lease expires, worker-B reclaims it. An acknowledgement carrying worker-A's token updates zero rows; worker-B's token marks the row delivered. The fixture also checks that a delivered row cannot be claimed again. Counting affected rows makes a lost lease visible instead of treating every acknowledgement as success.
The token belongs in the update predicate with the event ID and incomplete state. A plain update by event ID lets a slow former worker mark another owner's attempt complete. The lease lesson shows how the owner changes. A unique token should be generated per claim attempt, not reused as a permanent worker name in production; this fixture's readable names are only deterministic test markers.
Fencing protects the row, not the destination
The old worker could already have sent an event before its stale acknowledgement failed. The token prevents a stale database state transition but cannot retract an external message. Keep the event ID stable across retries and make the destination mutation idempotent. Also record failed acknowledgement attempts so an operational view can distinguish a lease loss from a database outage.
Checked source
update delivery_outbox
set delivered = true
where event_id = ? and claim_token = ? and delivered = false;Verification boundary
OutboxLeaseContractTest.expiredLeaseCanBeReclaimedButOldWorkerCannotAcknowledge runs in the downloadable Spring source kit. The excerpt is shortened; the kit contains the complete tests.
Costs and limits
This H2 row-count test proves a conditional state transition in one database. It does not include token generation, concurrent worker scheduling, send acknowledgements, broker retries or dead-letter handling. Each acknowledgement is one indexed row update when event_id is a primary key.
Common Mistakes
- Do not acknowledge by event ID alone after introducing leases.
- Do not reuse a token across distinct claim attempts.
- Do not call a fenced row update exactly-once message delivery.
Read next
Spring outbox claims: allow recovery after a worker lease expires, Spring consumer deduplication: commit the event ID with the mutation, Spring transactional outbox: commit a receipt and event row together.
