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

Spring consumer deduplication: commit the event ID with the mutation

Last updated: 30 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A consumer can make repeated delivery harmless by recording an event ID and its business mutation in one database transaction.

Download Spring source kit

The key and stock change share one commit

The source-kit fixture starts with 10 units for SKU-41. Event E-41 adds four; a repeated E-41 hits the primary key on applied_event and the stock stays at 14. A different E-42 removes two, leaving 12. Spring's TransactionTemplate groups the ledger insert and stock update; the duplicate exception is observed outside that transaction. This checks local database effects, not broker acknowledgement.

Place the event key in a uniqueness constraint rather than only checking a map or doing a read-before-write. Under concurrent consumers, a separate read can see no row twice; the database constraint decides which insert wins. A production handler should classify a true duplicate and acknowledge it without repeating the mutation, while treating an event ID reused with different payload as a conflict. The fixture rejects both by key and does not compare payload hashes.

Know the remaining gap

If the database commits and the message acknowledgement fails, the broker may redeliver. The ledger makes that repeat a no-op for this database state. If the handler also sends mail, calls another service or publishes outside the transaction, those effects need their own idempotency keys. Pair this with fenced outbox acknowledgement at the sender and atomic outbox write at its origin.

Checked source

Java
transaction.executeWithoutResult(status -> {
    jdbc.update("insert into applied_event (event_id) values (?)", eventId);
    jdbc.update("update stock_balance set available = available + ? where sku = ?",
        units, "SKU-41");
});

Verification boundary

ConsumerDedupContractTest.repeatedEventIdCannotApplyTheStockMutationTwice runs in the downloadable Spring source kit. The excerpt is shortened; the kit contains the complete tests.

Costs and limits

Two small H2 tables check one duplicate and one distinct event. A primary-key probe adds write and index cost; retained IDs consume storage and need an expiry policy tied to the broker's maximum redelivery window. This fixture does not test multiple consumers, payload mismatch classification, nonnegative inventory or remote side effects.

Common Mistakes

  • Do not check for an ID and then write without a database uniqueness constraint.
  • Do not record the ID in a different transaction from the business change.
  • Do not treat duplicate suppression as proof that all external effects are idempotent.

Read next

Spring transactional outbox: commit a receipt and event row together, Outbox acknowledgements: require the current lease token, Spring POST idempotency keys: replay the same receipt result.

Continue with checked relay behavior

Continue with Spring consumer idempotency: reserve an event ID with the stock mutation.

spring
spring-boot
consumer-dedup-transaction
Storage details