Contents

Use .choice().when(predicate).otherwise().end() to route messages to different endpoints based on header values, body content, or any predicate. Only the first matching when branch executes.

import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class ContentBasedRouter extends RouteBuilder { @Override public void configure() { from("direct:orders") .choice() .when(header("orderType").isEqualTo("EXPRESS")) .log("Express order → fast lane") .to("direct:expressProcessing") .when(header("orderType").isEqualTo("STANDARD")) .log("Standard order → normal lane") .to("direct:standardProcessing") .when(header("amount").isGreaterThan(1000)) .log("High-value order → review queue") .to("direct:reviewQueue") .otherwise() .log("Unknown order type — sending to DLQ") .to("direct:deadLetter") .end(); } } Always include .otherwise() to handle messages that match no when clause. Without it, unmatched messages pass through the choice block silently with no processing — which is rarely what you want.

Camel provides a rich set of built-in predicates for use inside .when():

PredicateExample
Header equalityheader("type").isEqualTo("A")
Header containsheader("name").contains("urgent")
Header regexheader("sku").regex("^[A-Z]{3}\\d{4}$")
Header numeric comparisonheader("qty").isGreaterThan(100)
Header is null / not nullheader("correlationId").isNull()
Body contains stringbody().contains("ERROR")
Simple expressionsimple("${header.priority} == 'HIGH'")
XPath (for XML bodies)xpath("/order/status = 'APPROVED'")
JSONPath (for JSON bodies)jsonpath("$.status == 'APPROVED'")
Compound (and/or)header("a").isEqualTo("X").and(header("b").isGreaterThan(0))
from("direct:xml-orders") .choice() .when(xpath("/order/priority = 'URGENT'")) .to("direct:urgent") .when(jsonpath("$.status == 'PENDING'")) .to("direct:pending") .when(simple("${header.amount} > 500 && ${header.currency} == 'USD'")) .to("direct:highValueUsd") .otherwise() .to("direct:default") .end();

.filter(predicate) drops (stops processing of) any message that does not satisfy the predicate. Messages that match continue through the route; non-matching messages are discarded silently unless you configure a filterRejected endpoint.

import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class MessageFilterRoute extends RouteBuilder { @Override public void configure() { // Only process events with severity ERROR or CRITICAL from("direct:events") .filter(header("severity").in("ERROR", "CRITICAL")) .log("Critical event: ${body}") .to("direct:alerting"); // Filter using a Simple expression from("direct:transactions") .filter(simple("${header.amount} > 1000")) .to("direct:largeTransactions"); // Filter using XPath on an XML body from("direct:xmlFeed") .filter(xpath("/item/category = 'tech'")) .to("direct:techNews"); } } Unlike .choice(), .filter() does not have an "else" branch. If you need to route non-matching messages elsewhere, use .choice().when(predicate)...otherwise()... instead.

The Recipient List EIP sends a copy of the message to a dynamic list of endpoints determined at runtime. The list can come from a header, a body field, or a custom bean.

import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class RecipientListRoute extends RouteBuilder { @Override public void configure() { // Header contains comma-separated endpoints: "direct:a,direct:b,seda:c" from("direct:broadcast") .recipientList(header("destinations")) .parallelProcessing() // send to all endpoints concurrently .end(); // Compute destinations dynamically via a bean from("direct:dynamicBroadcast") .recipientList(method(DestinationResolver.class, "resolve")) .ignoreInvalidEndpoints() .end(); } } import org.apache.camel.Header; import org.springframework.stereotype.Component; @Component public class DestinationResolver { public String[] resolve(@Header("orderRegion") String region) { return switch (region) { case "EU" -> new String[]{"direct:euWarehouse", "direct:euAudit"}; case "APAC" -> new String[]{"direct:apacWarehouse"}; default -> new String[]{"direct:defaultWarehouse"}; }; } }

The Dynamic Router calls a bean or expression repeatedly to decide the next endpoint until it returns null (which signals the end of routing). Unlike Recipient List, it routes the same exchange sequentially through multiple endpoints.

import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class DynamicRouterRoute extends RouteBuilder { @Override public void configure() { from("direct:dynamic") .dynamicRouter(method(StepRouter.class, "route")); } } import org.apache.camel.Exchange; import org.springframework.stereotype.Component; @Component public class StepRouter { public String route(Exchange exchange) { int step = exchange.getProperty("step", 0, Integer.class); exchange.setProperty("step", step + 1); return switch (step) { case 0 -> "direct:validate"; case 1 -> "direct:enrich"; case 2 -> "direct:persist"; default -> null; // null ends the dynamic routing }; } }

Patterns compose naturally in the Java DSL. A common pattern is to filter first, then route by content, then use a Recipient List for fan-out:

import org.apache.camel.builder.RouteBuilder; import org.springframework.stereotype.Component; @Component public class CombinedRoute extends RouteBuilder { @Override public void configure() { from("direct:incoming") // 1. Drop any test messages .filter(header("env").isNotEqualTo("TEST")) // 2. Route by order type .choice() .when(header("type").isEqualTo("B2B")) .to("direct:b2bPipeline") .when(header("type").isEqualTo("B2C")) .to("direct:b2cPipeline") .otherwise() .to("direct:unknownTypeDlq") .end(); // 3. Inside b2bPipeline: fan-out to multiple systems from("direct:b2bPipeline") .setHeader("destinations", constant("direct:erp,direct:crm,direct:audit")) .recipientList(header("destinations")) .parallelProcessing() .end(); } }

Tips for maintainable routing logic: