A database uniqueness constraint can make one local stock mutation safe under repeated delivery of the same event ID.
Spring Kafka duplicate delivery: let a unique event ID decide the second debit
Reject repetition inside the transaction
The fixture stores EVT-47 with its SKU and unit count in applied_reservation, then changes SKU-47 from 19 to 12 in one transaction. Calling the listener again with the same event ID hits the primary key. It acknowledges a replay only after checking the stored SKU and amount; a conflicting replay remains unacknowledged. No preflight SELECT makes the initial decision: two concurrent consumers could both observe absence before either inserts. The database ledger lesson explains the same ownership rule without a Kafka method around it.
A failure after marker insertion but before the stock update rolls the marker back. In the checked insufficient-stock case, the available value stays 19 and the applied-event count stays zero. The listener leaves the record unacknowledged so the container error policy can decide what happens next. Repeating that record forever would not create stock; failure classification needs an explicit policy.
Scope the key to the real event identity
The fixture uses one global event-ID primary key. A multi-tenant service may need a composite tenant-and-event identity, plus validation that the authenticated event source is allowed to affect that tenant. A reused event ID with different payload is a conflict worth recording, not automatically proof that the second payload is harmless. Retain the marker at least as long as the system can redeliver the event.
Code boundary
transactions.executeWithoutResult(status -> {
jdbc.update("insert into applied_reservation(event_id, sku, units) values (?, ?, ?)",
reservation.eventId(), reservation.sku(), reservation.units());
int updated = jdbc.update(
"update stock_balance set available = available - ? where sku = ? and available >= ?",
reservation.units(), reservation.sku(), reservation.units());
if (updated != 1) throw new IllegalStateException("stock item absent or insufficient");
});Verification boundary
The Java 21 / Spring Boot 4 source kit checks acknowledgesDuplicateWithoutSecondDebit, conflictingDuplicateIsNotAcknowledged and insufficientStockRollsBackMarkerAndDoesNotAcknowledge.
Cost and limits
The event marker adds one indexed row per retained event, so storage and retention need capacity planning. The local H2 test calls the listener sequentially. Concurrent inserts, deadlocks, isolation and constraint translation must be checked on the production database.
Common Mistakes
- Do not use a read-before-write check as the only duplicate guard.
- Do not commit the marker in a different transaction from the stock mutation.
- Do not discard marker rows while old records can still return.
Read next
Spring Kafka manual acknowledgement: commit work before advancing the offset, Spring consumer idempotency: reserve an event ID with the stock mutation, Spring transaction isolation: state the anomaly you need to prevent, Spring Kafka listener failures: separate transient retries from poison records, Spring Kafka record keys: preserve per-SKU order without claiming global order.
