Enterprise Integration Patterns
The vocabulary of system integration: routers, splitters, aggregators, content enrichers and the claim check, implemented with Apache Camel and Spring Integration.
On this page
Enterprise Integration Patterns gave the industry a shared vocabulary for connecting systems. The value today is less in the implementations — brokers and frameworks provide those — than in the names, which let a team describe a design precisely in a sentence.
Key Takeaways
- Content-based router sends a message to a destination chosen by its content.
- Splitter and aggregator decompose a composite message and reassemble the results.
- Content enricher adds data a message lacks by calling elsewhere.
- Claim check stores a large payload externally and passes a reference.
- Aggregators need a completion condition and a timeout, or they leak.
The patterns worth naming
| Pattern | What it does |
|---|---|
| Content-Based Router | Chooses a destination from message content |
| Message Filter | Discards messages that do not match |
| Splitter | Breaks a composite message into parts |
| Aggregator | Combines related messages into one |
| Resequencer | Restores order to out-of-sequence messages |
| Scatter-Gather | Broadcasts, then collects responses |
| Content Enricher | Adds missing data from an external source |
| Content Filter | Removes unneeded or sensitive fields |
| Claim Check | Stores the payload, passes a reference |
| Normalizer | Converts varied formats to a canonical one |
| Dead Letter Channel | Where undeliverable messages go |
One caution about the vocabulary before reaching for a framework: naming a pattern is not the same as
needing an engine to implement it. A content-based router is an if on a field. A splitter is a loop.
Camel earns its keep when you have many routes across varied transports and genuinely want its error
handling, redelivery and adapters — not when a single consumer branches three ways, where it adds a
DSL, a runtime and a debugging experience your team does not yet have.
Routing and splitting with Camel
@Component
public class OrderRoutes extends RouteBuilder {
@Override
public void configure() {
// Errors are part of the design, not an afterthought.
errorHandler(deadLetterChannel("kafka:orders.dlq")
.maximumRedeliveries(3)
.redeliveryDelay(1000)
.backOffMultiplier(2)
.retryAttemptedLogLevel(LoggingLevel.WARN));
// Content-based router
from("kafka:incoming-orders")
.routeId("order-router")
.unmarshal().json(OrderMessage.class)
.choice()
.when(simple("${body.totalMinorUnits} > 100000"))
.to("direct:high-value")
.when(simple("${body.region} == 'EU'"))
.to("direct:eu-processing")
.otherwise()
.to("direct:standard");
// Splitter with an aggregation strategy: one message per line, then
// recombine the per-line results into one summary.
from("direct:high-value")
.split(simple("${body.lines}"), new OrderLineAggregationStrategy())
.parallelProcessing()
.to("direct:process-line")
.end()
.to("kafka:orders-processed");
// Content enricher: the message lacks customer detail, so fetch it.
from("direct:process-line")
.enrich("direct:fetch-customer", (original, resource) -> {
OrderLine line = original.getIn().getBody(OrderLine.class);
Customer customer = resource.getIn().getBody(Customer.class);
original.getIn().setBody(line.enrichedWith(customer));
return original;
})
.to("bean:pricingService?method=calculate");
}
}The choice block is a content-based router, and its readability is the argument for a routing DSL:
the whole decision is visible in six lines rather than distributed across a service class.
parallelProcessing() on the splitter is worth noting — splits are independent by definition, so
processing them concurrently is usually free throughput.
Aggregation
from("kafka:line-results")
.aggregate(header("orderId"), new OrderResultAggregationStrategy())
.completionSize(header("expectedLineCount"))
// Without this, a lost message means this group is held forever.
.completionTimeout(30_000)
.aggregationRepository(persistentRepository) // survives a restart
.to("direct:finalise-order");Two production requirements that get missed. The timeout bounds memory — an aggregator with only a size condition holds incomplete groups indefinitely when one part never arrives. And a persistent repository means a restart does not lose in-flight groups; the in-memory default silently discards them.
Spring Integration
@Configuration
public class OrderIntegrationConfig {
@Bean
public IntegrationFlow orderProcessingFlow() {
return IntegrationFlow
.from(Kafka.messageDrivenChannelAdapter(consumerFactory, "incoming-orders"))
.transform(Transformers.fromJson(OrderMessage.class))
.filter((OrderMessage order) -> !order.lines().isEmpty())
.route(OrderMessage::region, mapping -> mapping
.subFlowMapping("EU", sub -> sub.channel("euChannel"))
.subFlowMapping("US", sub -> sub.channel("usChannel"))
.defaultOutputChannel("standardChannel"))
.get();
}
@Bean
public IntegrationFlow claimCheckFlow(ObjectStorage storage) {
return IntegrationFlow.from("largePayloads")
// Brokers are optimised for small messages. Store the payload,
// pass a reference, and let the consumer fetch it.
.handle((payload, headers) -> {
String key = storage.put((byte[]) payload);
return MessageBuilder.withPayload(new PayloadReference(key))
.copyHeaders(headers).build();
})
.channel("processedPayloads")
.get();
}
}The claim check is worth adopting whenever payloads exceed a few hundred kilobytes. A 50MB message on a queue degrades throughput for everything sharing that broker, and a reference plus an object-store fetch is both faster and cheaper.
Do you need a framework?
Be honest about the threshold. Two systems exchanging JSON over HTTP need a client class, not a routing engine. The framework earns its cost when there are many endpoints, several protocols, and routing rules that would otherwise be scattered across services with no single place to read them.
What you gain is a declarative description of the integration, 300+ prebuilt components, and error handling as a first-class concern. What you pay is a DSL your team must learn, stack traces that go through framework internals, and testing that needs its own harness.
A reasonable rule: if you can describe the integration in one clear paragraph, write the code. If it takes a diagram, the diagram is probably a Camel route.
Testing routes
@CamelSpringBootTest
@MockEndpoints("kafka:*")
class OrderRoutesTest {
@Autowired ProducerTemplate producer;
@EndpointInject("mock:kafka:orders-processed") MockEndpoint processed;
@Test
void routesHighValueOrdersSeparately() throws Exception {
processed.expectedMessageCount(1);
processed.expectedHeaderReceived("priority", "HIGH");
producer.sendBody("direct:incoming", highValueOrder());
processed.assertIsSatisfied(5_000);
}
}Mock endpoints let you assert on what reached a destination without a broker running, and
assertIsSatisfied with a timeout handles the asynchrony without a sleep.
What to take away
Learn the pattern names — they make integration designs describable in a sentence. Use a content-based router to keep decisions in one readable place, always give aggregators a completion timeout and a persistent repository, and reach for the claim check before large payloads reach a broker. Then check whether you need a framework at all.
Frequently Asked Questions
Camel or Spring Integration?
Do I need an integration framework at all?
How does an aggregator know when it is done?
Related tutorials
- Database Sharding & Scaling StrategiesScaling past one database: read replicas and routing, choosing a shard key you will not regret, hash versus range sharding, cross-shard queries, and migrating without downtime.
- gRPC in Java MicroservicesgRPC for internal service calls: Protocol Buffers and schema evolution, the four RPC types, deadlines and interceptors, Spring Boot integration, and an honest comparison with REST.
- API Security & the OWASP API Top 10The API-specific vulnerability classes and their Spring fixes: broken object-level authorization, mass assignment, unrestricted consumption, SSRF, and API inventory management.
- Transaction Management Deep DiveTransactions beyond the annotation: every propagation mode and when it applies, isolation levels and the anomalies they prevent, transaction-bound events, and why XA lost to sagas.