MuleESB / Apache Camel Interview Questions
Apache Camel is an open-source Java integration framework implementing the Enterprise Integration Patterns (EIPs) catalogue. It provides a routing and mediation engine with DSLs in Java, XML, YAML, and Groovy, letting developers define integration flows at a high level of abstraction rather than writing raw transport code.
Core problems it solves:
- Protocol heterogeneity: Translating between HTTP, JMS, AMQP, FTP, Kafka, gRPC, WebSocket, and 300+ more without custom per-protocol adapters.
- Data format conversion: Transforming between JSON, XML, CSV, Avro, HL7, EDI using built-in data-format components.
- Content-based routing: Directing messages to different systems based on headers, body, or custom predicates.
- System mediation: Connecting legacy ERP/CRM, cloud APIs, and brokers without tight coupling.
Camel ships with 300+ components and is an embeddable library — included in a Spring Boot or Quarkus app. Red Hat Fuse and Red Hat Integration are commercial distributions built on Camel.
Enterprise Integration Patterns (EIPs) are a catalogue of 65 named solutions to recurring messaging and integration problems, published by Gregor Hohpe and Bobby Woolf in 2003. Each pattern documents a proven approach — with a standard name, icon, and intent — for challenges such as routing, transformation, splitting, aggregating, and error handling in message-driven systems.
| EIP Pattern | Camel DSL | Purpose |
|---|---|---|
| Content-Based Router | choice()/when() | Route messages by content |
| Message Filter | filter(predicate) | Drop non-matching messages |
| Splitter | split(expression) | Break one message into many |
| Aggregator | aggregate(correlationId) | Combine many messages into one |
| Dead Letter Channel | deadLetterChannel(uri) | Safe failure routing after retries |
| Wire Tap | wireTap(uri) | Async side-copy for auditing |
| Recipient List | recipientList(expression) | Dynamic fan-out routing |
| Idempotent Consumer | idempotentConsumer(expr,repo) | Deduplicate messages |
| Content Enricher | enrich(uri) | Augment message from external source |
| Throttler | throttle(n) | Rate-limit message flow |
The Camel DSL maps directly to the EIP book language. The RouteBuilder is a composable canvas where EIPs chain together, replacing bespoke integration code with peer-reviewed building blocks that carry shared vocabulary across teams.
The Camel architecture has five cooperating building blocks:
- CamelContext: Runtime container owning all routes, components, endpoints, type converters, and thread pools.
- Route: A directed pipeline from(uri) through EIPs/Processors to to(uri). Each route has a unique ID.
- Endpoint: A named channel identified by URI. Acts as consumer in from() or producer in to().
- Component: Factory creating Endpoints for a URI scheme. FileComponent handles all file: URIs; auto-discovered via META-INF/services.
- Processor: Any object implementing org.apache.camel.Processor that mutates an Exchange. All EIP constructs compile to chained Processors.
CamelContext ctx = new DefaultCamelContext();
ctx.addRoutes(new RouteBuilder() {
public void configure() {
from("timer:tick?period=5000")
.process(e -> e.getMessage().setBody("ping"))
.to("log:output");
}
});
ctx.start();
A Route is the fundamental unit of integration logic — a complete message flow: where messages originate (from()), what processing they undergo (EIPs, Processors, Beans), and where they are sent (to()). Each route runs as an independent pipeline inside the CamelContext, identified by a unique route ID.
public class OrderRoutes extends RouteBuilder {
@Override
public void configure() {
from("file:/orders/input?noop=true")
.routeId("file-to-jms")
.log("Processing: ${header.CamelFileName}")
.unmarshal().csv()
.split(body()).to("jms:queue:order-items").end();
from("jetty:http://0.0.0.0:8080/api/orders")
.routeId("http-router")
.choice()
.when(header("type").isEqualTo("express"))
.to("jms:queue:express-orders")
.otherwise().to("jms:queue:standard-orders")
.end();
}
}Key rules: exactly one from(uri) per route; to(uri) sends to a producer without ending the route; routeId() assigns a stable ID for monitoring and testing. In Spring Boot, annotate a RouteBuilder subclass with @Component for automatic registration by camel-spring-boot.
The CamelContext is the central runtime container for a Camel application. It owns all routes, components, endpoints, type converters, the bean registry, data formats, and thread pools. Its lifecycle controls when routes are active and messages can flow.
| State | Method | What happens |
|---|---|---|
| Created | new DefaultCamelContext() | Object instantiated; no routes running. |
| Starting | context.start() | Components initialised; consumer endpoints begin polling. |
| Started | — | All routes active; messages flowing normally. |
| Suspending | context.suspend() | Consumers stop accepting; in-flight exchanges drain gracefully. |
| Suspended | — | Paused — useful for zero-downtime maintenance windows. |
| Resuming | context.resume() | Consumers reactivated. |
| Stopping | context.stop() | Graceful shutdown; thread pools terminated. |
| Stopped | — | Terminal state — create a new instance to restart. |
CamelContext ctx = new DefaultCamelContext();
ctx.addRoutes(new MyRouteBuilder());
ctx.start();
ctx.suspend(); // pause consumers gracefully
ctx.resume(); // re-activate consumers
ctx.stop(); // full shutdownIn Spring Boot, the CamelContext lifecycle is tied to the ApplicationContext. Individual routes can be started or stopped via context.startRoute(id) and context.stopRoute(id) without affecting other running routes.
An Endpoint is a communication channel that abstracts a specific transport behind a uniform interface. Every endpoint is identified by a URI: scheme:path?option1=value1. The scheme identifies the Component factory, the path is the component-specific address, and options configure behaviour as query parameters.
// File: poll dir every 2s, leave files untouched
from("file:/data/input?noop=true&delay=2000")
// JMS: produce with client acknowledgement
.to("jms:queue:invoices?acknowledgementModeName=CLIENT_ACKNOWLEDGE")
// HTTP: outbound call with 30s connect timeout
.to("http://api.example.com/products?bridgeEndpoint=true&connectTimeout=30000");
// Kafka: consume from earliest offset
from("kafka:payments?brokers=localhost:9092&autoOffsetReset=earliest");
// Timer: fire every 10s starting immediately
from("timer:heartbeat?period=10000&delay=0");An endpoint can be a Consumer (in from()) or Producer (in to()). The timer: component is consumer-only; log: is producer-only; jms: and kafka: support both. Endpoints are cached by the CamelContext — the same URI reuses the same instance. Property placeholders ({{key}}) keep environment-specific values out of code.
The Exchange is the message-passing container that travels through an entire route. When a consumer endpoint receives input, Camel wraps it in an Exchange and passes that object through every Processor. Everything a Processor needs — payload, metadata, and context — is accessed through the Exchange.
- In-message (exchange.getIn()): The current message with body, headers, and attachments. Processors read and write to this.
- Out-message (exchange.getOut()): The reply in InOut exchanges. In Camel 3.x writing to getOut() is discouraged because it creates a fresh Message and silently drops all headers set by prior processors.
- Headers: A Map<String, Object> on the current message. Components auto-populate keys like CamelFileName and JMSMessageID.
- Properties: Exchange-wide key-value pairs that persist across EIP steps like split() or aggregate().
- ExchangePattern: InOnly (fire-and-forget) or InOut (request-reply).
- Exception: Captured via exchange.getException() when a Processor throws.
from("direct:start")
.process(exchange -> {
String body = exchange.getIn().getBody(String.class);
exchange.getIn().setHeader("processedBy", "OrderProcessor");
exchange.setProperty("orderId", "ORD-1234"); // survives split
exchange.getIn().setBody(body.toUpperCase());
});
A Camel Message (org.apache.camel.Message) is the envelope carried inside an Exchange. It has three parts: body (any Java object — String, byte[], InputStream, POJO, DOM — type-converted transparently via getBody(TargetClass.class)), headers (a Map<String, Object> of metadata auto-populated by transport components), and attachments (named DataHandler objects for SOAP/MIME multipart messages).
In vs Out messages: In an InOnly exchange, only the In message exists; JMS publish, Kafka produce, and file write are InOnly by default. In an InOut exchange, the caller sends an In message and waits for a reply via the Out message; HTTP REST calls and synchronous direct: calls use InOut.
In Camel 3.x, the idiomatic reply approach is to mutate exchange.getIn() with the response body rather than writing to exchange.getOut(). Calling getOut() creates a fresh Message and silently discards all headers set by prior processors — a common source of bugs during Camel 3.x migrations.
A Processor is the atomic unit of message manipulation in Camel — any class implementing org.apache.camel.Processor. Every EIP construct compiles to Processors chained in a Pipeline. A custom Processor gives direct access to the full Exchange: body, headers, properties, and exception state.
The interface declares one method:
public interface Processor {
void process(Exchange exchange) throws Exception;
}A custom Processor that validates an order and stamps an audit header:
public class OrderValidationProcessor implements Processor {
@Override
public void process(Exchange exchange) throws Exception {
String body = exchange.getIn().getBody(String.class);
if (body == null || body.isBlank()) {
exchange.setException(new IllegalArgumentException("Empty payload"));
return;
}
exchange.getIn().setHeader("X-Validated-At",
java.time.Instant.now().toString());
exchange.getIn().setBody(body.trim());
}
}
from("jms:queue:raw-orders")
.process(new OrderValidationProcessor())
.to("jms:queue:valid-orders");
// Lambda shorthand:
from("direct:greet")
.process(e -> e.getIn().setBody("Hello " + e.getIn().getBody(String.class)));Processor vs Bean: Use Processor when you need direct Exchange-level access (headers, exceptions). Use .bean(MyBean.class) when the logic is pure business code — keeping domain classes free of Camel API imports and independently unit-testable.
The Content-Based Router (CBR) routes each incoming message to exactly one destination based on its content. In Camel it is implemented with choice() — when(predicate) — otherwise() — end(). Only the first matching when() branch executes; otherwise() catches messages that match no predicate.
from("direct:orders")
.choice()
.when(header("region").isEqualTo("EU"))
.to("jms:queue:eu-orders")
.when(header("region").isEqualTo("US"))
.to("jms:queue:us-orders")
.when(simple("${header.priority} == HIGH"))
.to("jms:queue:priority-orders")
.otherwise()
.to("jms:queue:default-orders")
.end();Predicates can use Simple, XPath, JSONPath, SpEL, or custom expressions. After .end() the route continues the normal flow. To continue processing AFTER every branch (not just the matching one), use endChoice() and add further steps. The when() predicate has access to the full Exchange including headers, body, and properties.
The Message Filter allows only messages that satisfy a predicate to continue in the route. Messages that do not match are silently dropped (the Exchange is stopped). It is the simplest routing EIP in Camel and is a special case of the Content-Based Router with no otherwise() branch.
from("jms:queue:events")
.filter(header("eventType").isEqualTo("ORDER_PLACED"))
.to("jms:queue:order-events");
// Filter using Simple expression:
from("direct:payments")
.filter(simple("${body.amount} > 1000"))
.to("jms:queue:large-payments");
// Filter with custom Predicate:
from("direct:in")
.filter(exchange -> exchange.getIn().getBody(String.class).startsWith("VIP"))
.to("direct:vip-handler");A filtered (dropped) message triggers no exception — it simply stops routing. To perform a side action on dropped messages, add a wireTap() before the filter. The filter() method accepts any object implementing the Predicate interface, including Simple, XPath, JSONPath, and Java lambda expressions.
The Splitter breaks a single message into multiple sub-messages, processes each independently, and optionally aggregates the results. It is used when a single inbound message contains a collection of items (a CSV line batch, a JSON array, an XML node list) that must be processed individually.
// Split a CSV line batch into individual records:
from("file:/in?noop=true")
.unmarshal().csv()
.split(body()) // splits List into individual rows
.parallelProcessing() // optional: process sub-messages in parallel
.streaming() // optional: do not buffer the whole list
.to("jms:queue:order-items")
.end(); // end of splitter sub-route
// Split XML using XPath:
from("direct:xmlOrders")
.split(xpath("//order"))
.to("direct:processOrder");Each sub-message gets a copy of the original headers plus Camel-generated metadata: CamelSplitIndex (0-based position), CamelSplitSize (total count), and CamelSplitComplete (true on last item). The sub-route between split() and end() is a full processing pipeline. Use streaming() for large collections to avoid loading everything into memory.
The Aggregator collects multiple messages that share a common correlation key and merges them into a single output message. It is the inverse of the Splitter and is needed when you receive many individual records (orders, events, sensor readings) that must be batched together before forwarding.
An aggregator requires two things: a correlation expression (how to group messages) and a completion condition (when to emit the aggregated message). Completion can be triggered by:
completionSize(n): emit when n messages are collected.completionTimeout(ms): emit when the oldest message in the group is ms milliseconds old.completionInterval(ms): emit every ms milliseconds regardless of size.completionPredicate(expr): emit when a custom predicate on the aggregated body is satisfied.completionFromBatchConsumer(): use CamelBatchComplete header set by batch-aware consumers like file: and JPA.
from("jms:queue:order-items")
.aggregate(header("orderId"), new GroupedBodyAggregationStrategy())
.completionSize(10)
.completionTimeout(5000)
.to("direct:sendBatch");
// Custom aggregation strategy:
public class ConcatStrategy implements AggregationStrategy {
public Exchange aggregate(Exchange old, Exchange newEx) {
String body = old == null ? ""
: old.getIn().getBody(String.class);
body += newEx.getIn().getBody(String.class) + ",";
newEx.getIn().setBody(body);
return newEx;
}
}
The Recipient List dynamically determines the list of endpoints to send a message to at runtime, based on a header or expression. Unlike Multicast (which uses a static list), the destinations are computed from the message itself. This is useful for subscription-based routing, workflow dispatch tables, and tenant-aware fan-out.
// Recipients read from a header (comma-separated URIs):
from("direct:start")
.recipientList(header("destinations"));
// Dynamic list built using Simple:
from("direct:notify")
.recipientList(simple("jms:queue:${header.tenantId}-alerts,http://audit.service/log"))
.parallelProcessing();
// Stop on first exception from any recipient:
from("direct:send")
.recipientList(header("targets")).stopOnException();The recipients are resolved per-message. Multiple recipients can be processed in parallel with .parallelProcessing(). Responses from each recipient are discarded by default; use an AggregationStrategy to merge them. The delimiter defaults to comma but can be changed with .delimiter(string).
The Wire Tap sends a copy of the current message to a secondary endpoint asynchronously while allowing the original message to continue through the route unchanged. It is used for auditing, logging to an append-only store, event sourcing side-channels, and monitoring without adding latency to the main processing path.
from("jms:queue:orders")
.wireTap("jms:topic:audit-log") // async copy to audit log
.to("direct:process-order"); // main flow continues
// Wire tap with body transformation for the copy:
from("direct:payment")
.wireTap("jms:queue:payment-audit")
.newExchangeBody(simple("Order ${header.orderId} received at ${date:now:yyyy-MM-dd}"))
.to("direct:process-payment");The wire tap sends a shallow copy of the Exchange with a new thread from the routing thread pool. The original Exchange continues immediately — there is no waiting for the tap. In Camel 3.x the tap always uses InOnly semantics; any response from the tap endpoint is discarded. The tap copy shares message headers with the original by default; use newExchangeBody() or onPrepare() to customise the tap message.
The Dead Letter Channel (DLC) is a default error handler that retries failed exchanges a configurable number of times and, on exhaustion, routes the Exchange to a designated dead-letter endpoint. It prevents message loss when downstream systems are temporarily unavailable.
// Configure Dead Letter Channel on the RouteBuilder:
public class MyRoutes extends RouteBuilder {
@Override
public void configure() {
errorHandler(deadLetterChannel("jms:queue:DLQ")
.maximumRedeliveries(3)
.redeliveryDelay(1000) // 1s initial delay
.backOffMultiplier(2.0) // doubles: 1s, 2s, 4s
.retryAttemptedLogLevel(LoggingLevel.WARN)
.logExhausted(true)
.useOriginalMessage()); // send original msg, not transformed
from("jms:queue:orders")
.to("http://payment-service/pay");
}
}When all retries are exhausted, Camel adds an exception header (CamelExceptionCaught) to the Exchange before routing to the DLQ. useOriginalMessage() ensures the pristine inbound message reaches the DLQ, not a partially transformed version. The DLC can be scoped at route level (inside configure()) or globally (in a parent class). For per-exception handling, combine with onException().
The Multicast EIP sends a copy of the current message to a fixed, statically defined list of endpoints. It is declared at route build time. Recipient List, by contrast, resolves the destination list dynamically from the message at runtime. Use Multicast when you always fan out to the same set of endpoints regardless of message content.
// Multicast to two fixed endpoints (sequential by default):
from("direct:order")
.multicast()
.to("jms:queue:billing", "jms:queue:shipping", "jms:queue:analytics")
.end();
// Parallel processing with aggregation:
from("direct:enrich")
.multicast(new MergeStrategy())
.parallelProcessing()
.to("direct:getCreditScore", "direct:getAddress", "direct:getHistory")
.end();| Aspect | Multicast | Recipient List |
|---|---|---|
| Destination list | Fixed at build time in the route definition | Computed from message header/expression at runtime |
| Typical use | Always fan out to the same set | Subscription-based or tenant-aware routing |
| Parallelism | Optional via parallelProcessing() | Optional via parallelProcessing() |
| Aggregation | Optional AggregationStrategy | Optional AggregationStrategy |
A Pipeline is the default message flow mechanism inside a Camel route. When you chain multiple to() or process() calls, Camel creates a Pipeline: the output (the In-message of the next step equals the Out-message of the previous step) flows sequentially from step to step. In Camel 3.x the exchange is mutated in-place via getIn(), so the Out-message concept is largely implicit.
Conceptually, a route IS a Pipeline — every step writes to exchange.getIn(), and the next step reads from exchange.getIn(). The pipeline() DSL method makes this explicit but is rarely needed since chained processors already form a pipeline by default.
// These two routes are equivalent:
// Implicit pipeline (default):
from("direct:start")
.process(new StepA())
.process(new StepB())
.to("mock:result");
// Explicit pipeline (same behaviour):
from("direct:start")
.pipeline()
.process(new StepA())
.process(new StepB())
.to("mock:result")
.end();The explicit pipeline() is useful when building sub-pipelines inside Multicast or Recipient List branches, where each branch needs its own independent processing chain. Outside of that context, a route already IS a pipeline and the explicit form adds no value.
The Content Enricher augments a message with data fetched from an external resource. Camel provides two variants:
- enrich(uri): Calls the external resource using a producer (e.g., HTTP GET, SQL query) and merges the response into the original message using an AggregationStrategy. The original Exchange is enriched in-place.
- pollEnrich(uri): Polls a consumer endpoint (e.g., file: or jms:) to fetch a resource and merges it. Useful for fetching reference data from a file or a JMS queue.
// enrich: HTTP GET to add product details to an order:
from("jms:queue:orders")
.enrich("http://product-service/api/products",
new AggregationStrategy() {
public Exchange aggregate(Exchange orig, Exchange resource) {
String product = resource.getIn().getBody(String.class);
orig.getIn().setHeader("productDetails", product);
return orig;
}
})
.to("direct:process");
// pollEnrich: fetch the latest reference file:
from("direct:start")
.pollEnrich("file:/data/config?fileName=rates.csv", 3000,
new FileBodyMergeStrategy())
.to("direct:applyRates");The key difference: enrich() PUSHES to the resource endpoint (producer call, needs active request); pollEnrich() PULLS from the resource endpoint (consumer poll). If pollEnrich() times out before finding data (timeout in ms, or -1 to wait indefinitely), it returns the original Exchange unchanged.
The Throttler limits the rate at which messages are forwarded to a downstream endpoint. It ensures the consumer does not receive more than N messages per time period, protecting rate-limited APIs and preventing downstream overload.
// Allow at most 10 messages per second:
from("jms:queue:events")
.throttle(10).timePeriodMillis(1000)
.to("http://api.example.com/event");
// Dynamic rate from a header (expressions supported):
from("direct:in")
.throttle(header("maxRate")).timePeriodMillis(1000)
.to("direct:downstream");
// Async throttle: do not block the calling thread:
from("direct:bulk")
.throttle(50).timePeriodMillis(1000).asyncDelayed()
.to("direct:target");Excess messages are delayed, not dropped — they are queued internally until the rate window opens. By default, the throttler uses a synchronized counter. Use asyncDelayed() to release the calling thread while the delayed Exchange waits; this is important for high-throughput scenarios to avoid thread starvation. The rate can be set dynamically per-Exchange using an expression.
The Idempotent Consumer deduplicates messages by tracking message IDs in a repository. If a message with the same ID is received again (e.g., after a retry or redelivery), it is silently dropped, ensuring each logical message is processed exactly once.
// In-memory repository (dev/testing):
from("jms:queue:payments")
.idempotentConsumer(header("JMSMessageID"),
MemoryIdempotentRepository.memoryIdempotentRepository(5000))
.to("direct:processPayment");
// JDBC repository (production, survives restarts):
JdbcMessageIdRepository repo =
new JdbcMessageIdRepository(dataSource, "paymentRoute");
from("jms:queue:payments")
.idempotentConsumer(header("JMSMessageID"), repo)
.to("direct:processPayment");
// Infinispan (distributed cache for clusters):
InfinispanIdempotentRepository infinispanRepo = ...
from("kafka:topic:payments?brokers=k:9092")
.idempotentConsumer(header("kafka.KEY"), infinispanRepo)
.to("direct:process");Camel supports multiple idempotent repository implementations: MemoryIdempotentRepository (in-memory, non-persistent), JdbcMessageIdRepository (persistent via JDBC), JpaMessageIdRepository (JPA), InfinispanIdempotentRepository (distributed cache), and HazelcastIdempotentRepository (Hazelcast). The repository stores message IDs for a configurable TTL or indefinitely.
The Saga EIP implements the Saga pattern for managing long-running distributed transactions without using two-phase commit (2PC). A saga is a sequence of local transactions coordinated by a compensation log: if any step fails, previously completed steps are rolled back via compensating transactions.
The Saga EIP is used when you need consistency across microservices that each own their own database and cannot share a single ACID transaction — for example, booking a flight, hotel, and car in one business flow where each service is independent.
from("direct:book-trip")
.saga()
.compensation("direct:cancel-trip") // run if saga fails
.completion("direct:confirm-trip") // run on success
.to("direct:book-flight")
.to("direct:book-hotel")
.to("direct:charge-card")
.end();
from("direct:cancel-trip")
.to("direct:cancel-flight")
.to("direct:cancel-hotel");Camel integrates with the LRA (Long Running Actions) specification via the camel-lra component, which uses a coordinator service. The Saga EIP guarantees eventual consistency rather than strict ACID consistency. It is the right choice for microservice choreography where 2PC would create tight coupling or lock contention.
A Camel Component is the factory responsible for creating Endpoint instances for a given URI scheme. Over 300 components ship with Camel, discovered automatically via META-INF/services/org/apache/camel/component/ entries. You use a component by referencing its URI scheme in from() or to().
// Timer: trigger a route every 5 seconds
from("timer:heartbeat?period=5000")
.setBody(constant("health-check"))
.to("log:health");
// File: poll directory, move processed files
from("file:/in?move=processed&delay=2000")
.to("file:/out");
// HTTP: outbound POST with JSON body
from("direct:send")
.setHeader(Exchange.HTTP_METHOD, constant("POST"))
.setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
.to("http://api.example.com/orders");
// JMS: consume from queue, produce to topic
from("jms:queue:orders")
.to("jms:topic:order-events");All four components follow the same URI pattern: scheme:path?options. Timer and File are primarily consumer-only (from()) and producer-only respectively; HTTP and JMS support both roles. Components are thread-safe singletons in the CamelContext; the same instance creates all endpoints for its scheme.
The camel-file component polls a directory for new files and processes each one as a Camel Exchange. The file body defaults to a java.io.File or InputStream depending on configuration. After processing, the file can be moved, deleted, or left in place based on the move, delete, and noop options.
// Poll, process, and move to done/ on success:
from("file:/data/in?move=done&moveFailed=failed&readLock=changed")
.log("Processing: ${header.CamelFileName}")
.unmarshal().csv()
.split(body())
.to("jms:queue:order-items")
.end();
// Write a file from a route:
from("direct:writeReport")
.setHeader(Exchange.FILE_NAME, simple("report-${date:now:yyyyMMdd}.csv"))
.to("file:/data/out");Key file component options: noop=true (leave file; for testing), delete=true (delete after read), move=dirname (move to subdirectory), readLock=changed (wait until file stops growing), delay=ms (polling interval), include=regex (file name filter), sortBy=filename (processing order). The component sets the CamelFileName and CamelFileLastModified headers automatically.
Camel integrates with Kafka via the camel-kafka component, which wraps the native Kafka Java client. The URI scheme is kafka:topicName?brokers=...&options. It supports both consuming (from()) and producing (to()) messages, with full access to Kafka record metadata through Exchange headers.
org.apache.camel
camel-kafka
// Consumer: read from Kafka, process, produce to Kafka:
from("kafka:orders?brokers=localhost:9092&groupId=order-processor")
.log("Received from partition ${header.kafka.PARTITION} offset ${header.kafka.OFFSET}")
.unmarshal().json(Order.class)
.process(new OrderProcessor())
.marshal().json()
.to("kafka:processed-orders?brokers=localhost:9092");
// application.properties for Camel Spring Boot:
camel.component.kafka.brokers=localhost:9092
camel.component.kafka.security-protocol=SASL_SSL
camel.component.kafka.sasl-mechanism=PLAINKey consumer options: groupId (consumer group), autoOffsetReset (earliest/latest), maxPollRecords, pollTimeoutMs. Key producer options: partitionKey, compressionCodec. The component automatically sets headers: kafka.TOPIC, kafka.PARTITION, kafka.OFFSET, kafka.KEY. Use seekTo=beginning for replay. Manual offset commit is supported via allow.auto.create.topics and the KafkaManualCommit header.
The REST DSL is a domain-specific language layered on top of Camel HTTP transport components. It describes REST APIs in a declarative verb-and-path style. The underlying HTTP server is pluggable — Undertow, Jetty, Servlet, or Netty — chosen via restConfiguration().
public class OrderRestRoutes extends RouteBuilder {
@Override
public void configure() {
restConfiguration()
.component("undertow").host("0.0.0.0").port(8080);
rest("/api/orders")
.get("/{id}").produces("application/json")
.to("direct:getOrder")
.post("/").consumes("application/json")
.type(OrderRequest.class)
.to("direct:createOrder");
from("direct:getOrder")
.to("sql:SELECT * FROM orders WHERE id = :#${header.id}");
}
}
// Consuming an external REST endpoint:
from("direct:callProducts")
.to("rest:GET:/api/products?host=catalog.internal:8080");REST DSL routes handle JSON/XML binding automatically when a type() class is specified. The rest: component acts as a producer for calling external REST APIs. Use camel-openapi-java to generate an OpenAPI spec from REST DSL definitions. In Spring Boot, auto-configure the REST engine via camel.rest.component=servlet.
The camel-jms component wraps the Spring JMS template and listener container, providing JMS connectivity via the standard javax.jms API. It supports queues and topics, durable subscriptions, message selectors, and transacted sessions. camel-activemq extends camel-jms with ActiveMQ-specific optimisations including in-JVM broker embedding.
// Spring Boot configuration:
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=admin
spring.activemq.password=admin
// Route: consume with JMS transactions:
from("jms:queue:orders?transacted=true&concurrentConsumers=5")
.process(new OrderProcessor())
.to("jms:topic:order-events");
// Request-reply over JMS (InOut pattern):
from("direct:calcPrice")
.to("jms:queue:pricing-service?exchangePattern=InOut&replyTo=pricing-replies");
// Durable topic subscription:
from("jms:topic:product-updates?subscriptionDurable=true&clientId=app1&durableSubscriptionName=app1-sub")
.to("direct:handleUpdate");Key options: transacted=true wraps the consumer in a JMS local transaction (rolled back on route exception), concurrentConsumers sets the thread pool size, acknowledgementModeName controls ack mode. For request-reply, exchangePattern=InOut blocks until a reply arrives on the replyTo queue. camel-activemq can embed a broker in-process using the vm://localhost transport, which is invaluable for testing.
The Bean component invokes a method on a Spring/CDI/JNDI bean from within a route. It is the recommended way to call business logic without tying domain classes to the Camel API: the bean knows nothing about Camel — it just receives parameters and returns a value.
// Bean referenced by class:
from("jms:queue:orders")
.bean(OrderService.class, "processOrder")
.to("jms:queue:results");
// Bean by Spring bean name:
from("direct:validate")
.bean("orderService", "validate(Exchange)")
.log("Validated: ${body}");
// Parameter binding annotations in the bean class:
public class OrderService {
public Order processOrder(
@Body String rawJson,
@Header("orderId") String id,
@ExchangeProperty("tenantId") String tenant) {
return orderRepo.save(parse(rawJson, id, tenant));
}
}Camel uses reflection and the Bean Parameter Binding mechanism to automatically map Exchange data to method parameters: @Body injects the message body, @Header(name) injects a header, @ExchangeProperty(name) injects a property. If there are no annotations, Camel uses the body type to match. The return value of the method becomes the new message body.
Camel provides two primary database components: camel-sql for declarative SQL route integration and camel-jdbc for low-level JDBC execution. Both require a configured DataSource in the registry (injected via Spring or registered manually in CamelContext).
// SQL: SELECT, body becomes List of row maps
from("timer:poll?period=60000")
.to("sql:SELECT * FROM orders WHERE status=:#status")
.split(body()).to("direct:processOrder");
// SQL: INSERT with named parameters from headers
from("direct:storeOrder")
.to("sql:INSERT INTO orders(id,status) VALUES(:#orderId,:#status)");
// JDBC: execute query in message body
from("direct:runQuery")
.setBody(constant("SELECT product_id, price FROM products WHERE active=1"))
.to("jdbc:myDataSource");The SQL component uses :#paramName named parameters mapped from headers (for simple names) or Exchange properties. Results are a List<Map<String, Object>>. The JDBC component takes the SQL from the message body. The header CamelJdbcRowCount gives the number of rows returned. For transactional execution, combine with a TransactionErrorHandler and a PlatformTransactionManager.
Camel Quarkus is the official integration of Apache Camel with the Quarkus framework. It packages Camel components as Quarkus extensions, enabling compile-time optimisation, GraalVM native image compilation, and sub-millisecond startup times. This makes it suitable for serverless functions, Kubernetes sidecar containers, and cost-sensitive cloud workloads.
Key capabilities of Camel Quarkus:
- Native compilation: GraalVM native-image eliminates JIT warmup; a Camel application can start in under 100ms and use 50-80% less memory than an equivalent JVM app.
- Dev mode: quarkus dev enables live reload of routes without restarting the JVM.
- Extension catalogue: 100+ Camel Quarkus extensions cover core EIPs, Kafka, HTTP, SQL, AWS S3/SQS, and more.
- MicroProfile Config integration: camel routes can read configuration from application.properties, environment variables, or Vault secrets.
- Kubernetes-native: Quarkus generates Kubernetes manifests and Health/Readiness probes automatically.
Routes in Camel Quarkus are defined exactly as in camel-spring-boot, using RouteBuilder subclasses annotated with @ApplicationScoped. The key difference is the build-time Quarkus extension mechanism that pre-initialises as much of the CamelContext as possible at compile time, eliminating reflection-heavy startup paths.
camel-spring-boot auto-configures a CamelContext as a Spring bean and binds its lifecycle to the Spring ApplicationContext. Any class annotated with @Component that extends RouteBuilder is automatically discovered and added. The starter also exposes Camel properties under the camel.* namespace in application.properties.
org.apache.camel.springboot
camel-spring-boot-starter
4.5.0
// Route class:
@Component
public class InvoiceRoute extends RouteBuilder {
@Value("${app.input-dir}") String inputDir;
@Override
public void configure() {
from("file:" + inputDir + "?noop=true")
.routeId("invoice-route")
.to("direct:process");
}
}
# application.properties
camel.springboot.name=MyApp
camel.springboot.routes-include-pattern=classpath:routes/*.yaml
app.input-dir=/data/invoicesIn addition to Java DSL routes, camel-spring-boot supports YAML and XML DSL route definitions loaded from the classpath via camel.springboot.routes-include-pattern. Beans in the Spring application context are available in Camel routes via .bean(BeanClass.class) or by Spring name. Health checks and JMX management are auto-configured when the relevant starters are on the classpath.
Apache Camel provides three complementary layers for data transformation:
- Type Converters: Implicit, automatic conversions between Java types (e.g., byte[] to String, File to InputStream). Applied transparently when you call getBody(TargetClass.class). Registered via @Converter annotations or explicit TypeConverterRegistry.
- Data Formats: Marshal (Java object —> bytes/String) and unmarshal (bytes/String —> Java object) operations. Used explicitly in routes with .marshal() and .unmarshal(). Formats include JSON (Jackson), XML (JAXB), CSV, Avro, Protobuf, EDI, HL7.
- Transformers: Route-level pipeline steps that use a processor, bean, or expression to reshape the body. This includes .transform(expression), .setBody(), .enrich(), and custom Processors.
// Type converter (implicit, no code needed in route):
String body = exchange.getIn().getBody(String.class); // auto-converts byte[]
// Data format: JSON marshal/unmarshal
from("direct:in")
.unmarshal().json(Order.class) // JSON bytes -> Order POJO
.process(new OrderProcessor())
.marshal().json() // Order POJO -> JSON bytes
.to("direct:out");
// Transformer using expression:
from("direct:transform")
.transform(simple("${body.toUpperCase()}"));Type Converters are always implicit — they run without any route configuration. Data Formats require explicit marshal()/unmarshal() calls and add the conversion to the pipeline. Transformers are explicit processing steps. Choosing between them: if you need structural format change (JSON to XML), use Data Formats; if you need business-level object manipulation, use Processors or Beans; implicit type coercion uses converters.
The Type Converter framework automatically converts a value from its current type to a requested target type. Every call to exchange.getIn().getBody(TargetClass.class) goes through the TypeConverterRegistry, which holds a lookup table of registered converters. Camel ships with hundreds of built-in converters (byte[] to String, InputStream to byte[], String to Integer, etc.).
To register a custom type converter, create a class annotated with @Converter(generateLoader=true) and annotate each conversion method with @Converter. Camel uses annotation processing at build time to generate the loader class, which is registered automatically via META-INF/services.
@Converter(generateLoader = true)
public final class OrderConverters {
@Converter
public static Order toOrder(String json, Exchange exchange) {
ObjectMapper om = exchange.getContext()
.getRegistry().lookupByNameAndType("objectMapper", ObjectMapper.class);
return om.readValue(json, Order.class);
}
@Converter
public static String fromOrder(Order order) {
return order.getId() + ":" + order.getAmount();
}
}
// Automatic use in route:
from("direct:in")
.process(e -> {
Order order = e.getIn().getBody(Order.class); // triggers custom converter
System.out.println(order.getId());
});The converter method signature supports optional second parameters of type Exchange or CamelContext for access to context. The converter is looked up by (from-type, to-type) pair. If no direct converter exists, Camel attempts multi-step conversion through intermediate types. Register converters programmatically via context.getTypeConverterRegistry().addTypeConverter(toType, fromType, converter) for dynamic registration.
Camel Data Formats are pluggable marshal/unmarshal strategies. You add them to a route using .marshal(dataFormat) (Java object to bytes/string) and .unmarshal(dataFormat) (bytes/string to Java object). Each Data Format is backed by a separate Maven dependency.
// JSON (Jackson):
from("direct:jsonIn").unmarshal().json(Order.class).to("direct:process");
// XML (JAXB):
JaxbDataFormat jaxb = new JaxbDataFormat("com.example.model");
from("direct:xmlIn").unmarshal(jaxb).to("direct:handleOrder");
// CSV (using OpenCSV):
from("file:/in?noop=true").unmarshal().csv().split(body()).to("direct:row");
// Avro:
AvroDataFormat avro = new AvroDataFormat(Order.SCHEMA);
from("kafka:orders?brokers=localhost:9092").unmarshal(avro).to("direct:handleAvro");
// Protobuf:
ProtobufDataFormat proto = new ProtobufDataFormat(OrderProto.OrderMessage.getDefaultInstance());
from("direct:protoIn").unmarshal(proto).to("direct:handleProto");Data Formats are registered by name in the CamelContext. You can also reference them by name in the Java DSL: .unmarshal("json"). In Spring Boot, Data Formats are auto-configured from classpath starters. Avro and Protobuf are used heavily in Kafka-based pipelines for binary efficiency; Jackson JSON is the default for REST integrations.
The camel-xslt component applies an XSLT stylesheet to the Exchange body (an XML document) and replaces the body with the transformed output. It is a pure producer component used in to() calls. The stylesheet is loaded once at route startup and cached for performance.
// Apply XSLT from classpath:
from("jms:queue:xml-orders")
.to("xslt:classpath:transforms/order-to-invoice.xsl")
.to("file:/out/invoices");
// Pass Exchange headers as XSLT parameters:
from("direct:transform")
.setHeader("tenantId", constant("ACME"))
.to("xslt:classpath:transforms/order.xsl?output=bytes");
XSLT stylesheets are loaded from classpath, file system, or HTTP. Exchange headers are passed to the stylesheet as XSLT parameters: a header named tenantId maps to the XSLT parameter . The output option can be string, bytes, file, or DOM. For Saxon XSLT 2.0/3.0 support, use the xslt-saxon component variant with the saxon:classpath: URI scheme.
Camel uses expressions and predicates everywhere a dynamic value is needed: filter(), when(), setHeader(), log(), split(), and idempotentConsumer(). Four languages are commonly used:
- Simple: Camel built-in. Resolves body, headers, properties, bean calls, date formatting, arithmetic. Zero extra dependency.
- SpEL (Spring Expression Language): Spring-only. Access Spring beans and methods.
- JSONPath: Evaluates a JSONPath expression against the JSON body.
- XPath: Evaluates an XPath expression against the XML body.
// Simple: filter by header, extract body field
from("direct:in")
.filter(simple("${header.priority} == HIGH"))
.to("direct:highPriority");
// XPath: extract attribute to header
from("direct:xml")
.setHeader("orderId", xpath("//order/@id", String.class))
.log("Order: ${header.orderId}");
// JSONPath: split JSON array
from("direct:jsonIn")
.split().jsonpath("$.orders[*]")
.to("direct:processItem");
// SpEL: call Spring bean
from("direct:validate")
.filter(spel("#{@orderService.isValid(body)}"))
.to("direct:valid");Simple is the right first choice for most routing predicates — it has zero overhead and no extra dependency. Use XPath when input is XML and you need node selection. Use JSONPath for JSON body queries. Use SpEL when you need to call Spring-managed beans inside predicates. Pre-compile Simple expressions at build time using the camel-maven-plugin to catch typos early.
Camel provides three built-in error handler implementations, configurable per-route via errorHandler(...):
| Handler | Retries | Use case |
|---|---|---|
| DefaultErrorHandler | Configurable (default 0) | Non-transactional routes that log failures. |
| DeadLetterChannel | Configurable (default 0) | Routes needing guaranteed delivery to a DLQ after retries exhausted. |
| TransactionErrorHandler | Driven by transaction manager | JMS/JDBC transactional routes requiring rollback on failure. |
| NoErrorHandler | None | Testing or scenarios where all error handling is done manually. |
// Default (no-DLQ): log on failure
errorHandler(defaultErrorHandler()
.maximumRedeliveries(2)
.redeliveryDelay(500));
// Dead Letter Channel
errorHandler(deadLetterChannel("jms:queue:DLQ")
.maximumRedeliveries(3)
.backOffMultiplier(2.0)
.useOriginalMessage());
// Transactional error handler
errorHandler(transactionErrorHandler(transactionManager)
.maximumRedeliveries(3)
.redeliveryDelay(1000));Error handlers can be scoped globally (in the parent RouteBuilder configure() or in a shared class) or per-route (placed at the start of a specific configure() method). Global scope applies to all routes defined in that builder. For per-exception handling, use onException() on top of any error handler to add targeted retry/routing logic for specific exception types.
onException(ExceptionClass.class) defines per-exception handling rules that override the route error handler for matched exception types. It must be declared BEFORE the from() in the RouteBuilder (in the configure() method). Multiple onException() clauses can coexist; Camel matches the closest superclass.
public void configure() {
// Per-exception handling
onException(IOException.class)
.maximumRedeliveries(5)
.redeliveryDelay(2000)
.backOffMultiplier(2.0)
.maximumRedeliveryDelay(30000) // cap at 30s
.retryAttemptedLogLevel(LoggingLevel.WARN)
.handled(true) // swallow the exception
.to("jms:queue:io-errors");
onException(ValidationException.class)
.handled(true) // mark Exchange as handled
.transform(exceptionMessage()) // body = exception message text
.to("direct:sendBadRequestResponse");
from("jms:queue:orders")
.to("http://payment-service/pay");
}Key configuration points: handled(true) marks the exception as consumed so Camel does not re-throw it; continued(true) marks it handled AND continues routing the original message; useOriginalMessage() restores the original body before sending to the DLQ. Exponential backoff is configured with backOffMultiplier() + maximumRedeliveryDelay(). For Camel Spring Boot, configure these via application.properties: camel.springboot.default-error-handler.maximum-redeliveries=3.
Camel supports JMS and JDBC transactions via the Spring PlatformTransactionManager. A transactional route marks a unit of work: if any step throws an exception, the transaction is rolled back and the message is redelivered by the broker (JMS) or a savepoint is rolled back (JDBC). The key moving parts are the JMS ConnectionFactory, a JmsTransactionManager, and the transacted=true route option.
// Spring Boot config (application.properties):
spring.activemq.broker-url=tcp://localhost:61616
// Transaction bean configuration:
@Bean
public PlatformTransactionManager jtaTransactionManager(ConnectionFactory cf) {
return new JmsTransactionManager(cf);
}
// Transactional route:
@Component
public class TxRoute extends RouteBuilder {
@Override
public void configure() {
from("jms:queue:orders?transacted=true")
.transacted() // enlist in Spring tx
.to("sql:INSERT INTO order_log VALUES (:#orderId)") // JDBC in same tx
.to("jms:queue:processed"); // JMS produce in same tx
}
}The .transacted() DSL method enlists the route in a Spring-managed transaction. Any exception causes rollback, and the JMS broker returns the message to the queue for redelivery. For XA (two-phase commit) across JMS + JDBC, replace JmsTransactionManager with a JTA-capable manager (Atomikos, Narayana). The TransactionErrorHandler is recommended alongside transacted() to control redelivery attempts before DLQ routing.
The Camel test kit allows you to stub real endpoints with in-memory MockEndpoints and set expectations on the messages they receive. The test extends CamelTestSupport, which boots a full in-memory CamelContext. MockEndpoints intercept to(uri) calls when you advise the route to replace real endpoints.
public class OrderRouteTest extends CamelTestSupport {
@Override
protected RoutesSupplier createRouteBuilder() {
return new OrderRoute(); // the route under test
}
@Test
public void testOrderRouted() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:jms:queue:processed");
mock.expectedMessageCount(1);
mock.expectedHeaderReceived("orderId", "ORD-001");
// Replace real endpoint with mock in route definition:
context.getRouteController().startRoute("order-route");
template.sendBodyAndHeader("direct:orders", "{amount:99}",
"orderId", "ORD-001");
mock.assertIsSatisfied();
}
}Use AdviceWith.adviceWith() to intercept and mock specific endpoints inside a route without modifying the route class. The ProducerTemplate (template field) lets you inject test messages into any direct: or other endpoint. MockEndpoint supports assertions on message count, body content, header values, received order, and no-messages-within-time. For Spring Boot integration tests, use @SpringBootTest with CamelSpringBootRunner.
CamelTestSupport (in the camel-test module) is the JUnit 4/5 base class that boots an isolated in-memory CamelContext for each test. It wires up the context, starts routes, and provides helper methods. When extending it, override createRouteBuilder() to supply the route under test.
@ExtendWith(CamelTestSupport.class)
public class PriceRouteTest extends CamelTestSupport {
@Override
protected RoutesSupplier createRouteBuilder() {
return new PriceRoute();
}
@Test
public void testDoublePriceTransformation() throws Exception {
// Use AdviceWith to intercept the real downstream endpoint:
AdviceWith.adviceWith(context, "price-route",
a -> a.mockEndpointsAndSkip("jms:*"));
MockEndpoint mock = getMockEndpoint("mock:jms:queue:output");
mock.expectedBodiesReceived("200.0");
template.sendBody("direct:price", 100.0);
mock.assertIsSatisfied();
}
}Key methods in CamelTestSupport: getMockEndpoint(uri) retrieves or creates a MockEndpoint, template is a ProducerTemplate for test message injection, assertMockEndpointsSatisfied() checks all mocks at once. AdviceWith lets you replace, insert, or skip route steps at test time without modifying the production route class — ideal for unit-testing complex multi-step routes. For Spring Boot integration testing, prefer @SpringBootTest with the camel-test-spring-junit5 module.
Camel exposes runtime metrics and management operations through three layers:
- JMX (Java Management Extensions): Enabled by default. Each CamelContext, Route, Endpoint, and Processor is a registered MBean. Accessible via JConsole, JMX clients, or Jolokia. Key operations: start/stop routes, view message counts, get exchange statistics.
- Camel Management API: Programmatic access to the same MBean data via context.getManagementStrategy(). Useful for embedding status checks in health endpoints.
- Micrometer: The camel-micrometer component registers Camel route/exchange metrics as Micrometer meters, which are scraped by Prometheus and displayed in Grafana. Add camel-micrometer-starter in Spring Boot for automatic registration.
# application.properties for Spring Boot metrics:
camel.metrics.enabled=true
management.endpoints.web.exposure.include=health,info,prometheus
management.metrics.export.prometheus.enabled=true
// Programmatic JMX access:
ManagedCamelContext mcc = context.adapt(ManagedCamelContext.class);
ManagedRouteMBean route = mcc.getManagedRoute("my-route-id", ManagedRouteMBean.class);
long count = route.getExchangesCompleted();
System.out.println("Completed: " + count);In Spring Boot, adding spring-boot-starter-actuator + camel-micrometer-starter automatically creates Camel route metrics named camel.route.exchanges.completed, camel.route.exchanges.failed, and camel.exchange.event.notifier counters. JMX can be disabled with camel.springboot.jmx-enabled=false. For Kubernetes, combine with the Liveness/Readiness health checks from camel-health and the Actuator health endpoint.
Camel K is a lightweight integration runtime designed for Kubernetes. Routes are deployed directly as YAML, Java, or Groovy DSL files — no Docker build, no Maven packaging, no Helm chart. The Camel K operator on the cluster compiles, packages, and deploys a minimal JVM container for each route file using a Just-In-Time build pipeline.
# Install the Camel K operator (via Helm or OperatorHub)
kubectl apply -f https://github.com/apache/camel-k/releases/latest/download/camel-k.yaml
# Deploy a route as a YAML file:
# orders-route.yaml
- from:
uri: timer:tick
parameters:
period: 5000
steps:
- setBody:
constant: "Hello from Camel K"
- log: "${body}"
# Apply directly to Kubernetes:
kamel run orders-route.yamlCamel K integrates with Knative for serverless scale-to-zero: when a route consumes from a Knative Eventing source, the operator configures the pod to scale down to zero when idle and scale up on incoming events. This makes it suitable for event-driven microservices and function-style integrations without the overhead of running idle JVM processes. Traits (compiler settings, Prometheus, Knative, Quarkus native) are configured via kamel run --trait flags.
Both Apache Camel and Spring Integration implement the EIP patterns from the Hohpe-Woolf book. They differ primarily in DSL style, ecosystem coupling, component breadth, and operational model:
| Aspect | Apache Camel | Spring Integration |
|---|---|---|
| DSL style | Fluent Java DSL, XML, YAML, Groovy — concise and readable | Annotation-driven (@InboundChannelAdapter, @Router) + XML; no single fluent API |
| Component breadth | 300+ components (all protocols and formats) | ~50 built-in adapters; relies on Spring ecosystem |
| Framework coupling | Framework-agnostic; embeds in Spring, Quarkus, or plain Java | Tightly coupled to Spring Framework |
| Serverless/Kubernetes | Camel K for Kubernetes-native; Camel Quarkus for native images | No dedicated serverless offering |
| Learning curve | Steeper: wide API surface with many options | Lower for Spring developers already familiar with Spring DI |
| Community | Apache Foundation; large open-source community | VMware/Broadcom; strong enterprise backing |
| Testing | CamelTestSupport, MockEndpoint, AdviceWith | MockIntegrationContext, Mockito-based |
| Stream processing | Limited; use Camel + Kafka Streams for pipelines | Spring Cloud Data Flow / Stream |
Choose Camel when you need a broad component catalogue, multi-DSL support, or cloud-native deployments (Camel K, Quarkus). Choose Spring Integration when your team is deeply invested in the Spring ecosystem and the integration patterns are limited to Spring-adjacent adapters. Both are mature frameworks with production deployments at scale.
Knowing what NOT to do in Camel prevents subtle bugs and performance degradation. Here are the most frequently encountered pitfalls:
- Writing to exchange.getOut(): Creates a new Message and silently drops all headers set by prior processors. Always mutate exchange.getIn() instead.
- Long-running Processors on consumer threads: Blocking a consumer thread starves the thread pool. Move heavy work into a SEDA or thread-pool-backed pipeline.
- Unbounded in-memory aggregation: Aggregator without a completionSize or completionTimeout will grow the aggregation repository indefinitely until OOM. Always set a completion condition.
- Using MemoryIdempotentRepository in production: It is not persistent and not cluster-safe. Use JDBC or Infinispan repositories.
- Creating Endpoints inside a Processor: Calling context.getEndpoint() in a tight loop creates many endpoint objects. Cache endpoints or use ProducerTemplate with pre-created templates.
- Forgetting .end() after split() or choice(): Without end(), subsequent steps are placed inside the sub-route or branch, not the main pipeline.
- Swallowing exceptions silently: Setting handled(true) without routing to a DLQ or logging means errors disappear. Always log or route failed Exchanges.
- Overusing direct: for high-throughput flows: direct: is synchronous and blocks the calling thread. Use seda: or thread() for async fan-out.
- Large message bodies held in memory: Streaming data (large files, video) must use streaming() in split() and stream-aware data formats. Buffering multi-GB files causes OOM.
- Unmanaged thread pools: Calling .threads() without configuring pool size or using default settings in high-concurrency routes leads to thread exhaustion.
In production reviews, the most common issue is the getOut() header-loss bug, followed by unbounded aggregators. A Camel route code review checklist should verify: completion conditions on every aggregator, getIn()-only mutations, DLQ routing after error handlers, and streaming flags on file/byte-array splits.
