Enterprise systems rarely speak the same language — messages fly between services in different formats, volumes, and destinations. Apache Camel gives you a battle-tested toolkit of Enterprise Integration Patterns (EIPs) that solve these routing and transformation challenges with elegant, reusable building blocks. In this lesson, you'll master four of the most powerful patterns: the Content-Based Router, Filter, Splitter, and Aggregator.
Back in 2003, Gregor Hohpe and Bobby Woolf published Enterprise Integration Patterns, a catalog of 65 solutions to recurring messaging problems. Think of them like design patterns (à la Gang of Four), but specifically for the world of message-driven systems. Apache Camel implements virtually all of them out of the box.
Every EIP in Camel fits into a route — a pipeline that describes how a message travels from a source (consumer) to a destination (producer). Along the way, EIPs act as processing stations that inspect, split, filter, or transform the message.
The mental model to keep in mind: imagine a postal sorting facility. Letters (messages) come in on a conveyor belt, workers (EIPs) read the labels, redirect them to the right chutes, tear open bulk packages into individual envelopes, discard junk mail, and bundle related letters into one bag. Each of those workers is an EIP.
Camel routes are written in its Domain-Specific Language (DSL), available in Java, XML (Spring/Blueprint), and YAML. This lesson uses the Java DSL since it's the most expressive for learning.
The four patterns we'll cover map naturally to those postal workers:
The Content-Based Router (CBR) is arguably the most commonly used EIP. Its job is simple: look at a message's content (headers, body, properties) and route it to one of several possible endpoints based on what it finds. This is exactly like a switch/case statement, but for message flows.
In Camel's Java DSL, you build a CBR using .choice(), followed by .when() clauses, and an optional .otherwise() fallback.
The .when() clause accepts a Predicate — any expression that evaluates to true or false. Camel ships with a rich expression language called Simple, as well as support for XPath, JsonPath, SpEL, and more.
Common pitfall: Forgetting .end() at the end of a .choice() block. Without it, subsequent steps in the route are ambiguous — Camel won't know if they belong inside the choice or after it. Always close your choice() with .end().
Another pitfall: using .choice() when you actually want different threads processing concurrently. CBR is a purely sequential decision — only one when() branch fires per message.
A powerful technique is combining CBR with header enrichment — setting metadata on the message before the router so that downstream systems know what decision was made, without having to re-inspect the body.
The Filter pattern is simpler than CBR: it has exactly one condition. If the condition is true, the message continues; if false, the message is dropped entirely — it doesn't go anywhere, not even to a fallback. This is the key distinction from CBR's otherwise().
Use Filter when you want to act on a subset of messages flowing through a route without changing the route's main logic. It's a gatekeeper, not a router.
In Camel's Java DSL, you use .filter():
from("activemq:topic:sensor-readings")
.filter(simple("${body.temperature} > 100"))
.to("activemq:queue:overheating-alerts");
Messages with temperature ≤ 100 are silently discarded. Only alerts that matter reach the overheating queue.
Why not just use choice().when().otherwise()? You could, but Filter is more declarative — it communicates intent clearly. When a reader sees .filter(), they immediately know "some messages are dropped here." With choice(), they need to check whether there's an otherwise() branch.
Note: A filtered-out message is not an error. Camel does not throw an exception or log a warning — it simply stops processing that exchange. If you need to know what was dropped (for auditing), add a
.log()or.to()inside a wrappingchoice().when(predicate).otherwise().stop()pattern instead.
Common pitfall: Expecting filtered messages to continue to the next .from() or route. They don't. A filtered message is gone. If you need to redirect rejects, use CBR.
The Splitter pattern solves a very common problem: you receive one message that contains many items (a batch order with 50 line items, a CSV file with 1,000 rows, a JSON array of events), and you need to process each item individually.
Camel's .split() breaks a single Exchange into multiple sub-exchanges, each carrying one item. These sub-exchanges travel the rest of the route independently.
You provide Camel an Expression that tells it how to split the body. For a Java List, it can split automatically. For XML, you'd use an XPath expression. For JSON arrays, JsonPath works perfectly.
By default, splitting is sequential — each sub-exchange is processed one after another. For high-throughput scenarios, you can add .parallelProcessing() to process sub-exchanges concurrently.
The aggregation callback: By default, after all splits are processed, Camel can optionally re-aggregate the results using a AggregationStrategy. If you pass one into .split(), the split results are collected back into a single exchange. If you don't, the sub-exchanges are fire-and-forget.
Common pitfall: Streaming large files. If you split a 2GB XML file by loading it entirely into memory first, you'll get an OutOfMemoryError. Use Camel's Streaming mode with .split().streaming() to process elements one at a time without loading the full document.
The Aggregator is the inverse of the Splitter — and the most complex of our four patterns. It collects multiple individual messages and combines them into a single, consolidated message. Think of it as reducing a stream of events down to a meaningful summary.
The Aggregator needs answers to three questions:
The correlation expression is the grouping key. For example, header("orderId") groups all messages that share the same order ID. Messages with different order IDs form separate groups simultaneously.
The AggregationStrategy is a Java interface with one method: aggregate(Exchange oldExchange, Exchange newExchange). You implement it to define how to merge a new incoming message into the accumulated result.
The completion condition tells Camel when to release the aggregated message downstream. You have several options:
.completionSize(N) — release when N messages have been collected.completionTimeout(ms) — release after a period of inactivity.completionPredicate(predicate) — release when a condition is met (e.g., a "last" flag in a header)Note: Camel's Aggregator stores in-progress groups in an AggregationRepository. By default this is in-memory, which means if the application crashes, partially aggregated groups are lost. For production systems, use a persistent repository backed by a database or Hazelcast.
In practice, you rarely use just one EIP at a time. The real power of Apache Camel emerges when you chain these patterns together to build sophisticated integration pipelines. A single route might route by content, filter noise, split a batch, and then aggregate results — each pattern handling its specific responsibility cleanly.
Consider a real-world scenario: a retail system receives bulk order files from multiple partners. Here's how the four patterns compose naturally:
partner header and routes XML files to an XML processor and CSV files to a CSV processororderId starting with "TEST-")This is the Pipes and Filters architectural style — each stage has a single responsibility and hands off to the next. This makes routes easy to test, reason about, and extend.
Testing tip: Camel provides a MockEndpoint (mock: URI scheme) that lets you assert how many messages arrived, in what order, and with what content. Combined with CamelTestSupport (JUnit), you can unit test each pattern in isolation without needing a real message broker.
Performance tip: When combining Splitter with Aggregator (a classic split-process-aggregate pipeline), make sure your Aggregation Repository can handle the concurrency level implied by your parallel split. An in-memory repository is fine for dev; in production, use a clustered store.
Note: Camel 3.x and 4.x have moved toward annotation-based and YAML-based route configuration. The Java DSL remains fully supported and is the best way to understand how patterns compose — learn it first, then adopt YAML DSL for declarative configuration management.
.choice().when().otherwise().end() to direct messages to different endpoints based on content — always close it with .end().streaming() for large files to avoid memory issuesApache Camel's Content-Based Router and Filter patterns are both used to control which messages proceed through a route, but they work in fundamentally different ways. Compare and contrast the Content-Based Router and Filter patterns: explain what each pattern does, what problem it solves, and describe a real-world scenario where you would choose one over the other. Use the postal sorting facility analogy from the lesson if it helps clarify your reasoning.