+1 (850) 665-2441

Content Enricher in Camel: enrich, pollEnrich, and Lookups That Don't Melt Your Backend

Most real messages arrive incomplete. An order carries a customer ID but not the credit tier. A shipment event has a SKU but no warehouse. The Content Enricher pattern from Hohpe and Woolf fixes this: call another resource, then merge the result into the message.

Camel gives you three ways to do that. They look alike and behave differently. This post covers which to pick and what goes wrong in production.

The three options

NeedUse
Call a producer endpoint, merge the reply into the current messageenrich
Read one message from a consumer endpoint (file, queue, SFTP), merge it inpollEnrich
Call an endpoint, discard the reply, keep the original bodyto (with InOut) or toD

The common mistake is using to and expecting the body to survive. A to to an HTTP or SQL endpoint replaces the body with the response. If you want the original plus something extra, you need enrich with an AggregationStrategy.

enrich with an AggregationStrategy

from("kafka:orders")
  .unmarshal().json(Order.class)
  .enrich("direct:customerLookup", (original, resource) -> {
      Order order = original.getIn().getBody(Order.class);
      Customer c = resource.getIn().getBody(Customer.class);
      order.setCreditTier(c.getTier());
      return original;
  })
  .to("direct:priceOrder");

from("direct:customerLookup")
  .setHeader("CamelHttpMethod", constant("GET"))
  .toD("https://crm.internal/customers/${body.customerId}")
  .unmarshal().json(Customer.class);

Two details matter here.

First, the resource route sees a copy of the exchange. Headers and body set inside direct:customerLookup do not leak back unless your strategy copies them. That is a feature. Keep it.

Second, put the lookup in its own route. You can then test it alone, apply error handling to it, and swap the implementation without touching the caller.

Dynamic endpoint URIs

enrich accepts a simple expression, so the endpoint can vary per message. Use it sparingly. Each distinct URI creates a producer, and Camel caches them in a bounded cache. Unbounded variety, such as one URI per customer, churns that cache and costs you connections. Pass variable data as a header or in the body, and keep the URI set small.

pollEnrich: read, don't call

pollEnrich consumes from an endpoint on demand. A typical use is pulling a reference file that a partner drops once a day.

.pollEnrich("file:/data/ref?fileName=rates.csv&noop=true", 5000)

The second argument is the timeout in milliseconds, and the choice carries weight:

  • -1: block until a message arrives. A missing file stalls the route thread indefinitely.
  • 0: return immediately if nothing is there.
  • A positive value: wait that long, then give up.

Avoid -1 on any route that serves live traffic. When the poll returns nothing, your AggregationStrategy receives a null resource exchange. Handle it explicitly, or you will get a NullPointerException that looks unrelated to the poll.

pollEnrich also does not suit high-volume per-message lookups. If you find yourself polling a file per message, the data belongs in a cache or a table.

Failure handling

A lookup is a network call, so it fails like one. Decide up front what a failed lookup means:

  1. The data is required. Let the exception propagate to your error handler and, after retries, to a dead letter channel. The message waits for a human or a replay.
  2. The data is optional. Catch the failure around the enrich and continue with a default. Mark the message, for example with a header enriched=false, so downstream steps and metrics can tell.
onException(HttpOperationFailedException.class)
  .onWhen(simple("${exchangeProperty.optionalLookup} == true"))
  .handled(true)
  .setHeader("enriched", constant(false));

Be honest about which case you are in. Silent defaults on required data are how bad prices reach customers.

If the lookup target is slow, apply a timeout on the client, and put a circuit breaker around it. A lookup without a timeout is a thread leak waiting for an incident.

Don't enrich per message when you can avoid it

A lookup per message turns your throughput into the backend's throughput. Alternatives, from cheapest to most involved:

  • Cache. Wrap the lookup route with a cache (Caffeine, via the caffeine-cache component, or Redis). Choose a TTL that matches how fast the reference data really changes. Stale tier data for five minutes is often fine. Say so explicitly in the design.
  • Batch. Aggregate N messages, make one bulk call, split the result. This works when the backend has a bulk API.
  • Join upstream. If both datasets are in Kafka, a Kafka Streams or ksqlDB join may beat any in-route lookup. Camel is not the right tool for stream-table joins. See our note on Camel versus plain Kafka consumers.
  • Fix the source. If the producer could include the field, ask. It is the cheapest enrichment there is.

Testing it

Because the lookup lives in its own route, replace it in tests with AdviceWith or a stub direct: route that returns a fixed reply. Then test three cases: lookup succeeds, lookup returns empty, lookup throws. Most enricher bugs live in the last two.

Checklist

  • Is the lookup in its own route?
  • Does the AggregationStrategy handle a null or empty resource?
  • Does every pollEnrich have a finite timeout?
  • Is the endpoint URI set bounded?
  • Is there a timeout and a defined outcome when the lookup fails?
  • Is per-message calling really necessary, or would a cache do?

If you have an estate full of ad hoc lookups and unclear failure behavior, that is the kind of thing an architecture review turns up quickly.