Idempotent Consumers in Camel: Surviving Redelivery Without Duplicates

Every delivery guarantee in a real integration estate rounds to at-least-once. Brokers redeliver after an unacknowledged crash. Error handlers replay after transient failures. Upstream producers time out and resend. Partners double-click. Exactly-once is a property of carefully bounded systems, not of the messy multi-protocol edges where Camel lives — so somewhere in your route, duplicates will arrive. The question is what they do when they get there.

If the route's endpoint is naturally idempotent — an upsert keyed on a business identifier, a file overwrite — you may need nothing more. But the moment a duplicate would double-charge, double-ship, or double-post, you need the Idempotent Consumer pattern, and Camel ships it as a first-class EIP.

The mechanics

from("jms:queue:payments")
    .idempotentConsumer(header("paymentId"),
        jdbcRepo)
    .to("bean:paymentProcessor");

The EIP checks the key against a repository; a first-seen key passes through and is recorded, a known key is silently skipped. Two design decisions dominate everything else: what is the key, and where does it live.

Choose a business key, not a transport id

The tempting key is the message id — JMSMessageID, a Kafka offset, a generated UUID header. It is also usually wrong, because transport ids identify deliveries, not intents. When an upstream producer times out and resends, the second message carries a new id and sails past your guard; the duplicate you most needed to catch is the one you missed.

The right key is the business identifier of the operation: payment id, order number plus event type, invoice number. If the payload has no such identifier, that is a conversation to have with the producing team before reaching for hashes of the body (which break the day a harmless field like a timestamp changes).

Choose a repository that matches your topology

  • MemoryIdempotentRepository is for tests and single-instance, restart-tolerant-loss cases only. It forgets on restart and is invisible to other nodes.
  • JDBC-backed repositories are the workhorse: durable, shared across instances, transactional with the rest of your database work, and easy to inspect when someone asks "did we already process this?"
  • Hazelcast / Infinispan / Redis-backed repositories fit clustered deployments that need lower latency than a database round-trip and can accept the cache's durability terms.

Whatever the store, plan for growth: an idempotency table with no eviction policy grows forever. Decide the deduplication window honestly — "we will never see a duplicate more than 30 days late" — and expire entries beyond it.

Two operational details are worth settling at design time. First, the repository is on the hot path: every message pays a lookup and a write, so index the key column properly and measure the cost against your peak rate before you discover it during a backlog replay. Second, make the table inspectable. Storing the key alongside a timestamp and the route id turns "did we already process invoice 88213, and when?" from an archaeology project into a one-line query — which is exactly the question someone will ask you during an incident.

Eager or not, and what failure does

By default the EIP is eager: the key is recorded before the exchange is processed, which closes the race where two identical messages arrive nearly simultaneously on different nodes. Pair it with removeOnFailure=true (also the default) so a failed exchange releases its key and a legitimate retry is not mistaken for a duplicate.

Note the tension: eager + remove-on-failure means a concurrent duplicate arriving while the first attempt is mid-flight gets skipped, and if that first attempt then fails, the operation happened zero times until redelivery. Non-eager closes that gap but opens the race instead. There is no free lunch — pick the failure mode your business prefers, and make sure redelivery is actually configured so "zero times, then retried" converges on once.

Idempotency is a property, not a component

The EIP guards entry into a route segment. It does not make the segment's side effects atomic. If your route writes a database row and calls a payment API, a crash between the two still leaves the world half-changed, and the idempotent consumer will cheerfully skip the redelivery that would have finished the job. For multi-effect routes, the guard belongs as close to each effect as possible — an upsert here, a provider idempotency key there — with the EIP handling the cheap, common case of whole-message duplicates.

The summary we give clients: assume duplicates, key on business identity, store keys durably and shared, expire honestly, and never confuse "we deduplicate at the door" with "our side effects are safe." That last sentence has paid for itself more than any other line in our review reports.