Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring Kafka duplicate delivery: let a unique event ID decide the second debit

Last updated: 1 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

A database uniqueness constraint can make one local stock mutation safe under repeated delivery of the same event ID.

Download Spring source kit

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

Java
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.

spring
spring-boot
kafka
kafka-idempotent-consumer
Storage details