The idempotent receiver interceptor detects repeated messages, but its default metadata store is process-local and its decision is separate from business state.
Spring Integration idempotent receiver: the metadata key is not the business commit
Choose the duplicate identity
A parcel scan from a handheld device needs a stable event ID, tenant ID and source revision. A timestamp header is not a safe business key: redelivery often has a new timestamp. MetadataStoreSelector can use an explicit key and a shared ConcurrentMetadataStore. By default, the selector's in-memory state disappears on restart, and a rejected message can still reach the handler marked as duplicate unless discard or rejection behavior is configured.
Close the atomicity gap
If the metadata store marks an ID seen and the business database transaction then rolls back, a retry can be incorrectly dropped. Put the dedup key and business write in one database transaction when that guarantee matters, using the unique-key consumer pattern. The interceptor remains useful as an early filter, but it is not proof of exactly-once effects across two stores.
Verify replay
Deliver event parcel-47 twice, restart the process, deliver it again, and compare rows and side effects. Then fail after metadata acceptance but before the business commit. This tests the gap that a duplicate-only unit test misses. The interceptor configuration below is an implementation sketch; connect it to a durable metadata store before using it for restart behavior.
Implementation sketch
@Bean
IdempotentReceiverInterceptor parcelDedup(ConcurrentMetadataStore store) {
MetadataStoreSelector selector = new MetadataStoreSelector(
message -> message.getHeaders().get("tenantId") + ":"
+ message.getHeaders().get("parcelEventId"), store);
return new IdempotentReceiverInterceptor(selector);
}Cost and verification
Each acceptance check adds metadata I/O if the store is shared. Keys need retention and a deletion policy; deleting them too early permits old redeliveries to run again.
Common Mistakes
- Do not use a changing timestamp as the duplicate identity.
- Do not assume rejected messages are discarded unless that behavior is configured.
- Do not treat a metadata-store write as atomic with a separate business database commit.
Read next
Spring consumer deduplication: commit the event ID with the mutation, Spring Integration queue: bound memory and choose a real message store, Spring Kafka duplicate delivery: let a unique event ID decide the second debit, Spring Integration poller: include receive and handler in the failure boundary.
