Enterprise Integration Patterns in Action with Apache Camel


In my previous article, I introduced Apache Camel as a framework that makes system-to-system integration cleaner and less painful. One of Camel’s biggest strengths is that it’s built on Enterprise Integration Patterns (EIPs) — proven design patterns for connecting systems reliably.
EIPs are like a cookbook for integration: “If you need to do X, here’s a standard way to do it.” Instead of reinventing the wheel (and writing endless glue code), you just apply the right pattern.
Let’s explore some of the most common patterns, why they matter, and how Camel makes them trivial to implement.
1. Content-Based Router
The problem:
Your service receives mixed messages — some go to billing, some to shipping, some to analytics. Without patterns, you’d end up writing nested if-else
or switch-case logic everywhere.
The pattern:
Content-Based Router sends a message to the right destination based on its content or metadata.
Camel Example:
from("jms:orders")
.choice()
.when(header("type").isEqualTo("electronics"))
.to("kafka:electronics-orders")
.when(header("type").isEqualTo("clothing"))
.to("kafka:clothing-orders")
.otherwise()
.to("kafka:general-orders");
Diagram:
flowchart LR
A[Orders Queue] -->|type=electronics| B[Kafka Electronics]
A -->|type=clothing| C[Kafka Clothing]
A -->|else| D[Kafka General]
How it works:
Reads messages from
orders
queue.Routes them by the
type
header.Keeps routing logic clean, extensible, and readable.
Real-life use case:
An e-commerce platform can route different product orders to specialized microservices without tangled conditional logic.
2. Splitter
The problem:
A single message may contain multiple items — like a CSV file with 1,000 rows — but you want to process each row individually.
The pattern:
Splitter breaks one message into many smaller ones.
Camel Example:
from("file:/input/orders.csv")
.split(body().tokenize("\n"))
.to("jms:singleOrder");
Diagram:
flowchart LR
A[CSV File] --> B[Splitter]
B --> C1[Order Line 1]
B --> C2[Order Line 2]
B --> C3[Order Line N]
How it works:
Reads a CSV file.
Splits each line into a separate message.
Pushes them to a queue for parallel processing.
Real-life use case:
Instead of trying to process a massive batch in one go, you can distribute workload across workers, improving scalability and fault tolerance.
3. Aggregator
The problem:
Sometimes you need the opposite of splitting — combine related messages into one (e.g., multiple partial shipments grouped into a single invoice).
The pattern:
Aggregator groups messages based on a correlation key.
Camel Example:
from("jms:partialOrders")
.aggregate(header("orderId"), new GroupedBodyAggregationStrategy())
.completionSize(5)
.to("jms:fullOrder");
Diagram:
flowchart LR
A1[Order Part 1] --> B[Aggregator]
A2[Order Part 2] --> B
A3[Order Part 3] --> B
A4[Order Part 4] --> B
A5[Order Part 5] --> B
B --> C[Complete Order]
How it works:
Groups messages by
orderId
.Combines 5 messages into one aggregated order.
Sends the result downstream.
Real-life use case:
A logistics app can wait for multiple package updates before sending a consolidated tracking update to the customer.
4. Message Filter
The problem:
Not all messages are worth processing — some are noise.
The pattern:
Filter lets only relevant messages pass through.
Camel Example:
from("direct:logs")
.filter(body().contains("ERROR"))
.to("slack:alerts");
Diagram:
flowchart LR
A[Log Stream] --> B[Filter ERROR only]
B -->|ERROR| C[Slack Alerts]
B -->|INFO/DEBUG| D[Discarded]
How it works:
Reads logs.
Passes only ERROR lines.
Sends alerts to Slack.
Real-life use case:
Ops teams get only actionable alerts, not flooded by INFO or DEBUG logs.
5. Wire Tap
The problem:
You want to send a copy of a message to a monitoring system without affecting the main flow.
The pattern:
Wire Tap makes a side copy of a message.
Camel Example:
from("jms:payments")
.wireTap("jms:audit")
.to("kafka:processPayments");
Diagram:
flowchart LR
A[Payments Queue] --> B[Wire Tap]
B --> C[Kafka Process Payments]
B --> D[Audit Queue]
How it works:
Sends payment messages to the main processing flow.
Simultaneously copies them to an audit queue.
Real-life use case:
Payment services can be monitored for fraud without slowing down the main transaction pipeline.
6. Enricher
The problem:
Sometimes messages are incomplete and need more data before processing (e.g., an order without customer details).
The pattern:
Enricher fetches additional information and enriches the message.
Camel Example:
from("jms:orders")
.enrich("direct:fetchCustomerDetails")
.to("kafka:enrichedOrders");
Diagram:
flowchart LR
A[Orders] --> B[Enricher]
B -->|Fetch Customer Data| C[Customer Service]
C --> B
B --> D[Enriched Orders]
How it works:
Reads orders.
Calls another route to fetch customer details.
Combines data and forwards enriched order.
Real-life use case:
ETL pipelines often need enrichment before storing data in a warehouse.
Why EIPs Matter
EIPs let you stop thinking in terms of custom code and start thinking in terms of patterns. With Camel, these patterns become one-liners instead of weeks of glue code.
That means:
Consistent solutions to recurring integration problems
Declarative, easy-to-read routes
Faster onboarding for new developers
Easier debugging and monitoring
Wrapping Up
Apache Camel isn’t just a framework — it’s a pattern engine. By applying EIPs, you can transform messy integration code into clean, modular, and reusable flows.
In this post, we covered:
Routing with Content-Based Router
Breaking down messages with Splitter
Combining data with Aggregator
Filtering noise
Creating audit trails with Wire Tap
Enriching messages with external data
But that’s only scratching the surface. Camel supports dozens of EIPs, from throttling and load balancing to resequencing and dynamic routing.
In my next article, I’ll dive deeper into error handling, retries, and dead letter queues — critical for production-ready integrations.
Subscribe to my newsletter
Read articles from Pragya Mutluru directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
