📦 Dependencies
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
</dependency>Gradle
implementation 'org.springframework.boot:spring-boot-starter-integration'
testImplementation 'org.springframework.integration:spring-integration-test'1. Core Concepts & Architecture
Spring Integration implements Enterprise Integration Patterns (EIP) within the Spring ecosystem. It connects components via messages flowing through channels, processed by endpoints.
Architecture Diagram
Producer ──→ [Channel] ──→ [Endpoint] ──→ [Channel] ──→ Consumer
│
┌─────────┴──────────┐
│ @ServiceActivator │
│ @Transformer │
│ @Filter │
│ @Router │
│ @Splitter │
│ @Aggregator │
└────────────────────┘Message = Header + Payload (immutable)
package com.example.integration;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
// Messages are immutable — use MessageBuilder to create
Message<String> message = MessageBuilder
.withPayload("Order #123")
.setHeader("orderId", 123)
.setHeader("priority", "HIGH")
.setHeader("region", "EU")
.build();
// Access
String payload = message.getPayload(); // "Order #123"
Object orderId = message.getHeaders().get("orderId"); // 123Enable Spring Integration
package com.example.integration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.config.EnableIntegration;
@SpringBootApplication
@EnableIntegration
@IntegrationComponentScan
public class IntegrationApplication {
public static void main(String[] args) {
SpringApplication.run(IntegrationApplication.class, args);
}
}When to Use Spring Integration vs Plain Spring Boot
| Scenario | Use |
|---|---|
| Simple REST API | Plain Spring Boot |
| Message routing between components | Spring Integration |
| File polling → transform → DB insert | Spring Integration |
| CRUD operations | Plain Spring Boot |
| Scatter-gather, split/aggregate | Spring Integration |
| Complex orchestration with retries | Spring Integration |
| Single external API call | Plain Spring Boot |
| Multiple protocols (HTTP + MQTT + JMS) | Spring Integration |
2. Message Channels — The Pipes
package com.example.integration.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.ExecutorChannel;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.channel.PriorityChannel;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import reactor.core.publisher.Flux;
import org.springframework.integration.channel.FluxMessageChannel;
@Configuration
public class ChannelConfig {
// Synchronous, point-to-point, single subscriber
@Bean
public DirectChannel orderChannel() {
return new DirectChannel();
}
// Async, polled, buffered — needs @Poller on consumer
@Bean
public QueueChannel processingQueue() {
return new QueueChannel(100); // capacity 100
}
// Broadcast to ALL subscribers
@Bean
public PublishSubscribeChannel eventBus() {
return new PublishSubscribeChannel();
}
// Priority-based ordering (messages implement Comparable or use comparator)
@Bean
public PriorityChannel priorityChannel() {
return new PriorityChannel(50); // capacity 50
}
// Async with thread pool
@Bean
public ExecutorChannel asyncChannel() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.initialize();
return new ExecutorChannel(executor);
}
// Reactive (Project Reactor)
@Bean
public FluxMessageChannel reactiveChannel() {
return new FluxMessageChannel();
}
}Global Channel Interceptor — Logging All Messages
package com.example.integration.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;
@Component
@GlobalChannelInterceptor(patterns = "*")
public class LoggingChannelInterceptor implements ChannelInterceptor {
private static final Logger log = LoggerFactory.getLogger(LoggingChannelInterceptor.class);
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
log.info("→ [{}] payload={}, headers={}",
channel, message.getPayload().getClass().getSimpleName(),
message.getHeaders());
return message;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
if (!sent) {
log.warn("✗ Message NOT sent to [{}]: {}", channel, message.getPayload());
}
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel,
boolean sent, Exception ex) {
if (ex != null) {
log.error("✗ Exception on [{}]: {}", channel, ex.getMessage(), ex);
}
}
}3. @ServiceActivator — The Workhorse
Basic Usage
package com.example.integration.service;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class OrderService {
// Basic: receives payload, returns result → sent to outputChannel
@ServiceActivator(inputChannel = "orderChannel", outputChannel = "processedChannel")
public String processOrder(String orderData) {
return "PROCESSED: " + orderData;
}
// Void return — fire-and-forget (one-way)
@ServiceActivator(inputChannel = "notificationChannel")
public void sendNotification(String message) {
System.out.println("Notification: " + message);
}
// Access full Message (headers + payload)
@ServiceActivator(inputChannel = "enrichChannel")
public Message<?> enrichOrder(Message<String> message) {
String enriched = message.getPayload() + " [enriched]";
return org.springframework.messaging.support.MessageBuilder
.withPayload(enriched)
.copyHeaders(message.getHeaders())
.setHeader("enriched", true)
.build();
}
// Individual header access
@ServiceActivator(inputChannel = "routedChannel")
public void handleWithHeader(String payload,
@Header("orderId") int orderId,
@Header(value = "priority", required = false) String priority) {
System.out.printf("Order %d (priority=%s): %s%n", orderId, priority, payload);
}
// All headers at once
@ServiceActivator(inputChannel = "auditChannel")
public void audit(String payload, @Headers Map<String, Object> headers) {
headers.forEach((k, v) -> System.out.printf(" %s = %s%n", k, v));
}
}Poller for QueueChannel + Retry Advice
package com.example.integration.service;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.stereotype.Component;
@Component
public class QueueConsumer {
// Poll from QueueChannel every 1000ms, max 10 messages per poll
@ServiceActivator(
inputChannel = "processingQueue",
poller = @Poller(fixedDelay = "1000", maxMessagesPerPoll = "10")
)
public void processFromQueue(String payload) {
System.out.println("Processing: " + payload);
}
}
@Configuration
class RetryConfig {
@Bean
public RequestHandlerRetryAdvice retryAdvice() {
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
RetryTemplate retryTemplate = new RetryTemplate();
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(1000);
backOff.setMultiplier(2.0);
backOff.setMaxInterval(10000);
retryTemplate.setBackOffPolicy(backOff);
advice.setRetryTemplate(retryTemplate);
return advice;
}
}Practical Example: Order Processing Service
package com.example.integration.service;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
@Component
public class OrderProcessingService {
@ServiceActivator(inputChannel = "validatedOrders", outputChannel = "enrichedOrders")
public Message<Order> enrichOrder(Order order) {
// Add tax, shipping, discount
order.setTax(order.getTotal() * 0.077); // Swiss VAT
order.setShipping(order.getTotal() > 100 ? 0 : 9.90);
return MessageBuilder.withPayload(order)
.setHeader("enrichedAt", System.currentTimeMillis())
.build();
}
@ServiceActivator(inputChannel = "enrichedOrders", outputChannel = "persistedOrders")
public Order persistOrder(Order order) {
// orderRepository.save(order);
return order;
}
@ServiceActivator(inputChannel = "persistedOrders")
public void notifyCustomer(Order order) {
// emailService.send(order.getEmail(), "Order confirmed: " + order.getId());
}
}4. @Transformer — Shape the Data
Basic Transformations
package com.example.integration.transformer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.integration.annotation.Transformer;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
@Component
public class OrderTransformer {
private final ObjectMapper objectMapper = new ObjectMapper();
// Basic: transform payload type
@Transformer(inputChannel = "rawOrders", outputChannel = "jsonOrders")
public String orderToJson(Order order) throws Exception {
return objectMapper.writeValueAsString(order);
}
// JSON string → Object
@Transformer(inputChannel = "jsonInput", outputChannel = "objectOutput")
public Order jsonToOrder(String json) throws Exception {
return objectMapper.readValue(json, Order.class);
}
// Header enrichment via MessageBuilder
@Transformer(inputChannel = "inbound", outputChannel = "enriched")
public Message<Order> enrichHeaders(Message<Order> message) {
Order order = message.getPayload();
return MessageBuilder.fromMessage(message)
.setHeader("customerTier", order.getTotal() > 1000 ? "GOLD" : "STANDARD")
.setHeader("processedAt", System.currentTimeMillis())
.setHeader("region", resolveRegion(order.getCountry()))
.build();
}
private String resolveRegion(String country) {
return switch (country) {
case "CH", "DE", "FR", "IT" -> "EU";
case "US", "CA" -> "US";
default -> "APAC";
};
}
}Practical Example: CSV → POJO → JSON Pipeline
package com.example.integration.transformer;
import org.springframework.integration.annotation.Transformer;
import org.springframework.stereotype.Component;
@Component
public class CsvTransformer {
// CSV line → POJO
@Transformer(inputChannel = "csvLines", outputChannel = "pojoChannel")
public Product csvToProduct(String csvLine) {
String[] parts = csvLine.split(",");
Product p = new Product();
p.setSku(parts[0].trim());
p.setName(parts[1].trim());
p.setPrice(Double.parseDouble(parts[2].trim()));
p.setQuantity(Integer.parseInt(parts[3].trim()));
return p;
}
// Sanitize — strip HTML, normalize whitespace
@Transformer(inputChannel = "pojoChannel", outputChannel = "sanitizedChannel")
public Product sanitize(Product product) {
product.setName(product.getName().replaceAll("<[^>]*>", "").trim());
return product;
}
}
// Java DSL alternative
@Configuration
class TransformerFlowConfig {
@Bean
public IntegrationFlow csvPipeline() {
return IntegrationFlow.from("csvInput")
.transform(String.class, csv -> {
String[] p = csv.split(",");
return new Product(p[0], p[1], Double.parseDouble(p[2]), Integer.parseInt(p[3]));
})
.transform(Product.class, product -> {
product.setName(product.getName().replaceAll("<[^>]*>", ""));
return product;
})
.channel("outputChannel")
.get();
}
}5. @Filter — Gate the Flow
Basic Filtering
package com.example.integration.filter;
import org.springframework.integration.annotation.Filter;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
@Component
public class OrderFilter {
// Return true = pass, false = discard
@Filter(inputChannel = "incomingOrders", outputChannel = "validOrders",
discardChannel = "rejectedOrders")
public boolean validateOrder(Order order) {
return order.getTotal() > 0
&& order.getEmail() != null && order.getEmail().contains("@")
&& order.getItems() != null && !order.getItems().isEmpty();
}
// throwExceptionOnRejection instead of discardChannel
@Filter(inputChannel = "strictChannel",
throwExceptionOnRejection = true)
public boolean strictValidation(Order order) {
return order.getTotal() > 0;
}
// Filter using header values
@Filter(inputChannel = "allOrders", outputChannel = "highPriorityOnly")
public boolean highPriorityFilter(Order order,
@Header("priority") String priority) {
return "HIGH".equals(priority);
}
}Practical Example: Deduplication Filter
package com.example.integration.filter;
import org.springframework.integration.annotation.Filter;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class DeduplicationFilter {
private final Set<String> seenIds = ConcurrentHashMap.newKeySet();
@Filter(inputChannel = "rawEvents", outputChannel = "uniqueEvents",
discardChannel = "duplicateEvents")
public boolean deduplicate(Event event) {
return seenIds.add(event.getId()); // returns false if already present
}
}
// Java DSL alternative
@Bean
public IntegrationFlow filterFlow() {
return IntegrationFlow.from("input")
.filter(Order.class, o -> o.getTotal() > 0,
f -> f.discardChannel("rejected").throwExceptionOnRejection(false))
.channel("validated")
.get();
}6. @Router — Direct Traffic
Returning Channel Names
package com.example.integration.router;
import org.springframework.integration.annotation.Router;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class OrderRouter {
// Route by region → returns channel name
@Router(inputChannel = "validatedOrders")
public String routeByRegion(Order order) {
return switch (order.getRegion()) {
case "EU" -> "euProcessingChannel";
case "US" -> "usProcessingChannel";
case "APAC" -> "apacProcessingChannel";
default -> "defaultProcessingChannel";
};
}
// Route to MULTIPLE channels (multicast)
@Router(inputChannel = "importantOrders")
public List<String> routeToMultiple(Order order) {
if (order.getTotal() > 10000) {
return List.of("processingChannel", "auditChannel", "notificationChannel");
}
return List.of("processingChannel");
}
// Route by header value
@Router(inputChannel = "allOrders")
public String routeByPriority(@Header("priority") String priority) {
return switch (priority) {
case "HIGH" -> "fastLaneChannel";
case "MEDIUM" -> "normalChannel";
default -> "batchQueueChannel";
};
}
}Built-in Routers (Java Config)
package com.example.integration.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.router.HeaderValueRouter;
import org.springframework.integration.router.PayloadTypeRouter;
@Configuration
public class RouterConfig {
// HeaderValueRouter — route based on header value
@Bean
public IntegrationFlow headerRouterFlow() {
return IntegrationFlow.from("inbound")
.<String, String>route(
m -> m.getHeaders().get("type", String.class),
mapping -> mapping
.subFlowMapping("ORDER", sf -> sf.channel("orderChannel"))
.subFlowMapping("INVOICE", sf -> sf.channel("invoiceChannel"))
.defaultOutputChannel("unknownChannel")
)
.get();
}
// PayloadTypeRouter — route by payload class
@Bean
public IntegrationFlow payloadTypeRouterFlow() {
return IntegrationFlow.from("mixedInput")
.<Object, Class<?>>route(Object::getClass,
mapping -> mapping
.subFlowMapping(Order.class, sf -> sf.channel("orderChannel"))
.subFlowMapping(Invoice.class, sf -> sf.channel("invoiceChannel"))
)
.get();
}
// RecipientListRouter — send to all matching recipients
@Bean
public IntegrationFlow recipientListFlow() {
return IntegrationFlow.from("events")
.routeToRecipients(r -> r
.recipient("auditChannel") // always
.recipient("alertChannel", // conditional
m -> "ERROR".equals(m.getHeaders().get("level")))
.recipient("metricsChannel")) // always
.get();
}
}7. @Splitter — Break It Apart
Basic Splitting
package com.example.integration.splitter;
import org.springframework.integration.annotation.Splitter;
import org.springframework.stereotype.Component;
import java.util.Iterator;
import java.util.List;
@Component
public class OrderSplitter {
// Split bulk order into individual items
// Spring auto-sets sequenceSize, sequenceNumber, correlationId headers
@Splitter(inputChannel = "bulkOrders", outputChannel = "individualItems")
public List<OrderItem> splitOrder(Order order) {
return order.getItems();
}
// Lazy/streaming split — returns Iterator (memory efficient)
@Splitter(inputChannel = "largeOrders", outputChannel = "streamedItems")
public Iterator<OrderItem> splitLazy(Order order) {
return order.getItems().iterator();
}
}Practical Example: CSV File → Rows
package com.example.integration.splitter;
import org.springframework.integration.annotation.Splitter;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class CsvFileSplitter {
@Splitter(inputChannel = "csvFiles", outputChannel = "csvRows")
public List<String> splitCsvFile(File file) throws Exception {
List<String> lines = Files.readAllLines(file.toPath());
// Skip header row, return data rows
return lines.stream()
.skip(1)
.filter(line -> !line.isBlank())
.collect(Collectors.toList());
}
}
// Java DSL alternative with built-in FileSplitter
@Bean
public IntegrationFlow fileSplitFlow() {
return IntegrationFlow.from("fileInput")
.split(Files.splitter()
.markers() // emit START/END markers
.charset("UTF-8")
.applySequence(true))
.channel("lineOutput")
.get();
}8. @Aggregator — Put It Together
Annotation-Based Aggregation
package com.example.integration.aggregator;
import org.springframework.integration.annotation.Aggregator;
import org.springframework.integration.annotation.CorrelationStrategy;
import org.springframework.integration.annotation.ReleaseStrategy;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class OrderAggregator {
// Correlate by orderId header — group messages belonging to the same order
@CorrelationStrategy
public Object correlateByOrderId(Message<?> message) {
return message.getHeaders().get("orderId");
}
// Release when all items received (sequenceSize == group size)
@ReleaseStrategy
public boolean releaseWhenComplete(List<Message<?>> messages) {
// Default: checks sequenceSize header matches group size
if (messages.isEmpty()) return false;
int expected = (int) messages.get(0).getHeaders().get("sequenceSize");
return messages.size() >= expected;
}
// Aggregate: combine all payloads into a result
@Aggregator(inputChannel = "processedItems", outputChannel = "completedOrders")
public Order aggregateOrder(List<OrderItem> items) {
Order order = new Order();
order.setItems(items);
order.setTotal(items.stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum());
return order;
}
}Java DSL with Timeout & MessageGroupStore
package com.example.integration.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.store.SimpleMessageStore;
import org.springframework.integration.jdbc.store.JdbcMessageStore;
import javax.sql.DataSource;
@Configuration
public class AggregatorConfig {
@Bean
public IntegrationFlow aggregatorFlow() {
return IntegrationFlow.from("splitItems")
.aggregate(a -> a
.correlationStrategy(m -> m.getHeaders().get("orderId"))
.releaseStrategy(g -> g.size() >= g.getOne().getHeaders()
.get("sequenceSize", Integer.class))
.groupTimeout(30_000) // 30s timeout → partial release
.sendPartialResultOnExpiry(true)
.messageStore(new SimpleMessageStore()) // or jdbcMessageStore()
.outputProcessor(g -> {
List<OrderItem> items = g.getMessages().stream()
.map(m -> (OrderItem) m.getPayload())
.toList();
return new Order(items);
})
)
.channel("aggregatedOrders")
.get();
}
// Persistent store for production (survives restarts)
@Bean
public JdbcMessageStore jdbcMessageStore(DataSource dataSource) {
return new JdbcMessageStore(dataSource);
}
}Practical Example: Scatter-Gather (3 services)
package com.example.integration.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
@Configuration
public class ScatterGatherConfig {
@Bean
public IntegrationFlow scatterGatherFlow() {
return IntegrationFlow.from("priceRequest")
.scatterGather(
scatterer -> scatterer
.applySequence(true)
.recipientFlow(sf -> sf.handle("supplierAService", "getPrice"))
.recipientFlow(sf -> sf.handle("supplierBService", "getPrice"))
.recipientFlow(sf -> sf.handle("supplierCService", "getPrice")),
gatherer -> gatherer
.releaseStrategy(g -> g.size() == 3) // wait for all 3
.groupTimeout(5000) // or timeout after 5s
)
// Select best price from gathered responses
.handle((prices, headers) -> {
@SuppressWarnings("unchecked")
List<PriceQuote> quotes = (List<PriceQuote>) prices;
return quotes.stream()
.min(Comparator.comparing(PriceQuote::getPrice))
.orElseThrow();
})
.channel("bestPrice")
.get();
}
}9. @Gateway — The Clean Entry Point
@MessagingGateway Interface
package com.example.integration.gateway;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.messaging.handler.annotation.Header;
import java.util.concurrent.Future;
// Hides ALL Spring Integration complexity from business code
@MessagingGateway
public interface OrderGateway {
// Synchronous: send and wait for reply
@Gateway(requestChannel = "orderInput", replyChannel = "orderOutput")
OrderResult submitOrder(Order order);
// With headers
@Gateway(requestChannel = "orderInput")
OrderResult submitOrder(Order order,
@Header("priority") String priority,
@Header("region") String region);
// Fire-and-forget (void return)
@Gateway(requestChannel = "notificationChannel")
void sendNotification(String message);
// Async — returns Future
@Gateway(requestChannel = "asyncProcessing")
Future<OrderResult> submitOrderAsync(Order order);
}Using the Gateway in Business Code
package com.example.integration.controller;
import com.example.integration.gateway.OrderGateway;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderGateway orderGateway;
public OrderController(OrderGateway orderGateway) {
this.orderGateway = orderGateway;
}
@PostMapping
public OrderResult createOrder(@RequestBody Order order) {
// Business code has ZERO knowledge of channels, messages, endpoints
return orderGateway.submitOrder(order, "HIGH", "EU");
}
}Reactive Gateway (Mono)
package com.example.integration.gateway;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.MessagingGateway;
import reactor.core.publisher.Mono;
@MessagingGateway
public interface ReactiveFileGateway {
@Gateway(requestChannel = "fileProcessingInput")
Mono<ProcessingResult> processFile(byte[] fileContent);
}10. @InboundChannelAdapter & @Poller — Produce Messages
Basic Polling
package com.example.integration.adapter;
import org.springframework.integration.annotation.InboundChannelAdapter;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.core.MessageSource;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class OrderPoller {
private final AtomicInteger counter = new AtomicInteger();
// Poll every 5 seconds
@InboundChannelAdapter(
channel = "polledOrders",
poller = @Poller(fixedDelay = "5000", maxMessagesPerPoll = "10")
)
public String pollForOrders() {
// In reality: query DB for unprocessed orders
return "Order-" + counter.incrementAndGet();
}
}Practical Example: Database Poller
package com.example.integration.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.jdbc.JdbcPollingChannelAdapter;
import javax.sql.DataSource;
@Configuration
public class DatabasePollerConfig {
@Bean
public JdbcPollingChannelAdapter jdbcMessageSource(DataSource dataSource) {
JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(
dataSource,
"SELECT * FROM orders WHERE status = 'NEW' ORDER BY created_at LIMIT 10"
);
adapter.setUpdateSql("UPDATE orders SET status = 'PROCESSING' WHERE id IN (:id)");
return adapter;
}
@Bean
public IntegrationFlow dbPollerFlow(JdbcPollingChannelAdapter jdbcMessageSource) {
return IntegrationFlow
.from(jdbcMessageSource,
c -> c.poller(Pollers.fixedDelay(5000).maxMessagesPerPoll(10)))
.channel("newOrdersChannel")
.get();
}
}File Poller → Process → Archive
package com.example.integration.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.FileWritingMessageHandler;
import org.springframework.integration.file.filters.SimplePatternFileListFilter;
import java.io.File;
@Configuration
public class FilePollerConfig {
@Bean
public FileReadingMessageSource fileSource() {
FileReadingMessageSource source = new FileReadingMessageSource();
source.setDirectory(new File("/data/incoming"));
source.setFilter(new SimplePatternFileListFilter("*.csv"));
source.setAutoCreateDirectory(true);
return source;
}
@Bean
public IntegrationFlow filePollerFlow(FileReadingMessageSource fileSource) {
return IntegrationFlow
.from(fileSource,
c -> c.poller(Pollers.fixedDelay(10_000))) // every 10s
.handle("csvProcessingService", "process")
// Archive processed file
.handle(new FileWritingMessageHandler(new File("/data/archive")))
.get();
}
}Cron-Based Polling
@InboundChannelAdapter(
channel = "reportChannel",
poller = @Poller(cron = "0 0 8 * * MON-FRI") // 8 AM weekdays
)
public Report generateDailyReport() {
return reportService.generate();
}11. Error Handling — Don't Lose Messages
Global Error Handler
package com.example.integration.error;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Component;
@Component
public class GlobalErrorHandler {
// All unhandled errors arrive here
@ServiceActivator(inputChannel = "errorChannel")
public void handleError(Message<MessagingException> errorMessage) {
MessagingException exception = errorMessage.getPayload();
Message<?> failedMessage = exception.getFailedMessage();
// Log
System.err.printf("ERROR: %s | Payload: %s | Headers: %s%n",
exception.getCause().getMessage(),
failedMessage != null ? failedMessage.getPayload() : "N/A",
failedMessage != null ? failedMessage.getHeaders() : "N/A");
// Store for later retry
// failedMessageRepository.save(failedMessage);
// Notify
// slackNotifier.send("Integration error: " + exception.getMessage());
}
}Retry with Exponential Backoff → Dead Letter Queue
package com.example.integration.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
@Configuration
public class RetryAndDlqConfig {
@Bean
public DirectChannel deadLetterChannel() {
return new DirectChannel();
}
@Bean
public RequestHandlerRetryAdvice retryAdvice() {
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3)); // max 3 attempts
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(1000); // 1s
backOff.setMultiplier(2.0); // 1s → 2s → 4s
backOff.setMaxInterval(10000); // cap at 10s
retryTemplate.setBackOffPolicy(backOff);
advice.setRetryTemplate(retryTemplate);
advice.setRecoveryCallback(ctx -> {
// After all retries exhausted → send to DLQ
Message<?> failed = (Message<?>) ctx.getAttribute("message");
deadLetterChannel().send(failed);
return null;
});
return advice;
}
@Bean
public IntegrationFlow retryFlow() {
return IntegrationFlow.from("riskyInput")
.handle("externalService", "call",
e -> e.advice(retryAdvice()))
.channel("successOutput")
.get();
}
}Per-Endpoint Error Channel
// Each endpoint can have its own error channel
@ServiceActivator(inputChannel = "paymentChannel")
@Bean
public IntegrationFlow paymentFlow() {
return IntegrationFlow.from("paymentInput")
.handle("paymentService", "charge",
e -> e.advice(retryAdvice()))
.get();
}
// Dedicated error handler for payment failures
@ServiceActivator(inputChannel = "paymentErrorChannel")
public void handlePaymentError(Message<MessagingException> error) {
// Refund logic, notify finance team, etc.
}12. Integration with External Systems
Add specific dependencies per adapter: spring-integration-jdbc, spring-integration-amqp, spring-integration-kafka, spring-integration-http, spring-integration-file, spring-integration-mqtt, spring-integration-sftp, spring-integration-websocket
JDBC — Poll & Insert
@Bean
public IntegrationFlow jdbcInboundFlow(DataSource ds) {
return IntegrationFlow
.from(new JdbcPollingChannelAdapter(ds,
"SELECT * FROM events WHERE processed = false"),
e -> e.poller(Pollers.fixedDelay(5000)))
.handle("eventProcessor", "process")
.get();
}
@Bean
public IntegrationFlow jdbcOutboundFlow(DataSource ds) {
return IntegrationFlow.from("processedEvents")
.handle(new JdbcMessageHandler(ds,
"INSERT INTO processed_events (id, data) VALUES (:headers[id], :payload)"))
.get();
}RabbitMQ (AMQP)
@Bean
public IntegrationFlow amqpInboundFlow(ConnectionFactory cf) {
return IntegrationFlow
.from(Amqp.inboundAdapter(cf, "order-queue")
.configureContainer(c -> c.prefetchCount(10)))
.handle("orderService", "process")
.get();
}
@Bean
public IntegrationFlow amqpOutboundFlow(AmqpTemplate amqpTemplate) {
return IntegrationFlow.from("outboundChannel")
.handle(Amqp.outboundAdapter(amqpTemplate)
.routingKey("processed-orders"))
.get();
}Kafka
@Bean
public IntegrationFlow kafkaInboundFlow(ConsumerFactory<String, String> cf) {
return IntegrationFlow
.from(Kafka.messageDrivenChannelAdapter(cf, "orders-topic")
.configureListenerContainer(c -> c.groupId("integration-group")))
.handle("orderHandler", "handle")
.get();
}
@Bean
public IntegrationFlow kafkaOutboundFlow(KafkaTemplate<String, String> kt) {
return IntegrationFlow.from("toKafka")
.handle(Kafka.outboundChannelAdapter(kt).topic("processed-orders"))
.get();
}HTTP — Inbound & Outbound
// Inbound: expose HTTP endpoint
@Bean
public IntegrationFlow httpInboundFlow() {
return IntegrationFlow
.from(Http.inboundGateway("/api/integration/orders")
.requestMapping(m -> m.methods(HttpMethod.POST))
.requestPayloadType(Order.class))
.handle("orderService", "process")
.get();
}
// Outbound: call external REST API
@Bean
public IntegrationFlow httpOutboundFlow() {
return IntegrationFlow.from("callExternalApi")
.handle(Http.outboundGateway("https://api.example.com/validate")
.httpMethod(HttpMethod.POST)
.expectedResponseType(ValidationResult.class))
.channel("validationResults")
.get();
}MQTT (see also: Spring Boot MQTT Cheatsheet)
@Bean
public IntegrationFlow mqttInboundFlow(MqttPahoClientFactory factory) {
return IntegrationFlow
.from(new MqttPahoMessageDrivenChannelAdapter("client-in", factory,
"sensors/+/temperature", "sensors/+/humidity"))
.handle("sensorDataService", "process")
.get();
}
@Bean
public IntegrationFlow mqttOutboundFlow(MqttPahoClientFactory factory) {
return IntegrationFlow.from("mqttOutbound")
.handle(new MqttPahoMessageHandler("client-out", factory))
.get();
}SFTP
@Bean
public IntegrationFlow sftpInboundFlow(SessionFactory<SftpClient.DirEntry> sf) {
return IntegrationFlow
.from(Sftp.inboundAdapter(sf)
.remoteDirectory("/remote/incoming")
.localDirectory(new File("/local/incoming"))
.patternFilter("*.csv"),
e -> e.poller(Pollers.fixedDelay(30_000)))
.handle("fileProcessor", "process")
.get();
}13. Testing Spring Integration
Test Setup
package com.example.integration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.integration.test.context.SpringIntegrationTest;
import org.springframework.integration.test.mock.MockIntegration;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@SpringIntegrationTest
@DirtiesContext
class OrderFlowIntegrationTest {
@Autowired
@Qualifier("orderInput")
private MessageChannel inputChannel;
@Autowired
@Qualifier("orderOutput")
private PollableChannel outputChannel;
@Test
void orderFlowProcessesCorrectly() {
// Arrange
Order order = new Order("ORD-001", "[email protected]", 150.00);
// Act
inputChannel.send(MessageBuilder.withPayload(order).build());
// Assert
var result = outputChannel.receive(5000);
assertThat(result).isNotNull();
assertThat(result.getPayload()).isInstanceOf(Order.class);
Order processed = (Order) result.getPayload();
assertThat(processed.getStatus()).isEqualTo("PROCESSED");
assertThat(result.getHeaders().get("enriched")).isEqualTo(true);
}
}Mock External Services
package com.example.integration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.test.context.SpringIntegrationTest;
import org.springframework.integration.test.mock.MockIntegration;
import org.springframework.integration.test.mock.MockMessageHandler;
@SpringBootTest
@SpringIntegrationTest
class OrderFlowMockedTest {
@Configuration
static class TestConfig {
// Mock the external payment service
@Bean
public MockMessageHandler paymentServiceMock() {
return MockIntegration.mockMessageHandler()
.handleNextAndReply(m -> {
Order order = (Order) m.getPayload();
return new PaymentResult(order.getId(), "PAID", "TXN-12345");
});
}
}
@Test
void orderFlowWithMockedPayment() {
// The real flow runs, but payment calls hit the mock
inputChannel.send(MessageBuilder.withPayload(testOrder).build());
var result = outputChannel.receive(5000);
assertThat(result).isNotNull();
// Verify the mock was called
// paymentServiceMock.verify(...);
}
}Verify Headers & Payload
@Test
void verifyHeadersAndPayload() {
inputChannel.send(MessageBuilder
.withPayload(new Order("ORD-002", "[email protected]", 500.0))
.setHeader("priority", "HIGH")
.build());
var result = outputChannel.receive(5000);
assertThat(result).isNotNull();
// Verify headers
assertThat(result.getHeaders())
.containsEntry("priority", "HIGH")
.containsEntry("enriched", true)
.containsKey("processedAt");
// Verify payload
Order order = (Order) result.getPayload();
assertThat(order.getTax()).isCloseTo(38.5, within(0.01)); // 500 * 0.077
assertThat(order.getShipping()).isEqualTo(0); // free above 100
}14. Real-World Patterns — Complete Flows
Pattern 1: Order Processing Pipeline
Gateway → Filter(validate) → Transformer(enrich) → Router(domestic/intl) ├── domesticChannel → ServiceActivator(process) → ServiceActivator(notify) └── internationalChannel → ServiceActivator(customs) → ServiceActivator(process) → ServiceActivator(notify)
package com.example.integration.order;
import org.springframework.integration.annotation.*;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import java.util.List;
// ── Gateway ──────────────────────────────────────────────────
@MessagingGateway
public interface OrderGateway {
@Gateway(requestChannel = "orderInput", replyChannel = "orderComplete")
OrderResult submit(Order order);
}
// ── Filter: Validate ─────────────────────────────────────────
@Component
class OrderValidator {
@Filter(inputChannel = "orderInput", outputChannel = "validOrders",
discardChannel = "invalidOrders")
public boolean validate(Order order) {
return order.getTotal() > 0
&& order.getEmail() != null
&& order.getItems() != null && !order.getItems().isEmpty();
}
}
// ── Transformer: Enrich ──────────────────────────────────────
@Component
class OrderEnricher {
@Transformer(inputChannel = "validOrders", outputChannel = "enrichedOrders")
public Message<Order> enrich(Order order) {
order.setTax(order.getTotal() * 0.077);
order.setTimestamp(System.currentTimeMillis());
String region = order.getCountry().equals("CH") ? "DOMESTIC" : "INTERNATIONAL";
return MessageBuilder.withPayload(order)
.setHeader("region", region)
.build();
}
}
// ── Router: Domestic vs International ────────────────────────
@Component
class RegionRouter {
@Router(inputChannel = "enrichedOrders")
public String route(@Header("region") String region) {
return region.equals("DOMESTIC") ? "domesticChannel" : "internationalChannel";
}
}
// ── ServiceActivator: Process ────────────────────────────────
@Component
class DomesticProcessor {
@ServiceActivator(inputChannel = "domesticChannel", outputChannel = "notifyChannel")
public Order process(Order order) {
order.setStatus("PROCESSED");
order.setTrackingNumber("CH-" + System.currentTimeMillis());
return order;
}
}
@Component
class InternationalProcessor {
@ServiceActivator(inputChannel = "internationalChannel", outputChannel = "notifyChannel")
public Order processIntl(Order order) {
order.setCustomsCleared(true);
order.setStatus("PROCESSED");
order.setTrackingNumber("INTL-" + System.currentTimeMillis());
return order;
}
}
// ── ServiceActivator: Notify ─────────────────────────────────
@Component
class OrderNotifier {
@ServiceActivator(inputChannel = "notifyChannel", outputChannel = "orderComplete")
public OrderResult notify(Order order) {
// emailService.send(order.getEmail(), "Order " + order.getId() + " confirmed");
return new OrderResult(order.getId(), order.getStatus(), order.getTrackingNumber());
}
}Pattern 2: File ETL Pipeline
FilePoller → Splitter(lines) → Transform(CSV→POJO) → Filter(valid) → Aggregate(batch of 100) → JDBC BulkInsert → Archive
package com.example.integration.etl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.*;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.filters.SimplePatternFileListFilter;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
class EtlFlowConfig {
@Bean
public FileReadingMessageSource csvSource() {
FileReadingMessageSource source = new FileReadingMessageSource();
source.setDirectory(new File("/data/incoming"));
source.setFilter(new SimplePatternFileListFilter("*.csv"));
return source;
}
@Bean
public IntegrationFlow etlFlow(FileReadingMessageSource csvSource) {
return IntegrationFlow
.from(csvSource, c -> c.poller(Pollers.fixedDelay(10_000)))
.channel("csvFiles")
.get();
}
}
@Component
class CsvLineSplitter {
@Splitter(inputChannel = "csvFiles", outputChannel = "csvLines")
public List<String> split(File file) throws Exception {
return Files.readAllLines(file.toPath()).stream()
.skip(1).filter(l -> !l.isBlank()).collect(Collectors.toList());
}
}
@Component
class CsvToProductTransformer {
@Transformer(inputChannel = "csvLines", outputChannel = "products")
public Product transform(String line) {
String[] p = line.split(",");
return new Product(p[0].trim(), p[1].trim(),
Double.parseDouble(p[2].trim()), Integer.parseInt(p[3].trim()));
}
}
@Component
class ProductValidator {
@Filter(inputChannel = "products", outputChannel = "validProducts",
discardChannel = "invalidProducts")
public boolean validate(Product p) {
return p.getPrice() > 0 && p.getQuantity() >= 0 && !p.getSku().isBlank();
}
}
@Configuration
class BatchAggregatorConfig {
@Bean
public IntegrationFlow batchAndInsert() {
return IntegrationFlow.from("validProducts")
.aggregate(a -> a
.correlationStrategy(m -> "BATCH") // single batch group
.releaseStrategy(g -> g.size() >= 100)
.groupTimeout(5000)
.sendPartialResultOnExpiry(true))
.handle("jdbcBulkInsertService", "insertBatch")
.get();
}
}Pattern 3: Scatter-Gather (Best Price)
Request → Scatter[SupplierA, SupplierB, SupplierC] → Gather(wait all or 5s) → Select best price → Reply
package com.example.integration.scatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.List;
@Configuration
class ScatterGatherFlow {
@Bean
public IntegrationFlow bestPriceFlow() {
return IntegrationFlow.from("priceRequest")
.scatterGather(
s -> s.applySequence(true)
.recipientFlow(f -> f.handle("supplierA", "quote"))
.recipientFlow(f -> f.handle("supplierB", "quote"))
.recipientFlow(f -> f.handle("supplierC", "quote")),
g -> g.releaseStrategy(group -> group.size() == 3)
.groupTimeout(5000)
.sendPartialResultOnExpiry(true)
)
.handle((payload, headers) -> {
@SuppressWarnings("unchecked")
List<PriceQuote> quotes = (List<PriceQuote>) payload;
return quotes.stream()
.min(Comparator.comparing(PriceQuote::getAmount))
.orElse(null);
})
.get();
}
}
@Component
class SupplierA {
@ServiceActivator
public PriceQuote quote(PriceRequest req) {
return new PriceQuote("SupplierA", req.getProductId(), 29.99);
}
}
@Component
class SupplierB {
@ServiceActivator
public PriceQuote quote(PriceRequest req) {
return new PriceQuote("SupplierB", req.getProductId(), 24.50);
}
}
@Component
class SupplierC {
@ServiceActivator
public PriceQuote quote(PriceRequest req) {
return new PriceQuote("SupplierC", req.getProductId(), 31.00);
}
}Pattern 4: Publish-Subscribe Event Bus
Event → PubSubChannel → [Audit Logger]
→ [Email Notifier]
→ [Metrics Collector]
→ [Cache Invalidator]package com.example.integration.pubsub;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.stereotype.Component;
@Configuration
class EventBusConfig {
@Bean
public PublishSubscribeChannel eventBus() {
return new PublishSubscribeChannel();
}
}
@Component
class AuditLogger {
@ServiceActivator(inputChannel = "eventBus")
public void audit(DomainEvent event) {
// auditRepository.save(new AuditEntry(event));
System.out.printf("[AUDIT] %s: %s at %s%n",
event.getType(), event.getEntityId(), event.getTimestamp());
}
}
@Component
class EmailNotifier {
@ServiceActivator(inputChannel = "eventBus")
public void notify(DomainEvent event) {
if ("ORDER_PLACED".equals(event.getType())) {
// emailService.send(event.getUserEmail(), "Order confirmed!");
}
}
}
@Component
class MetricsCollector {
@ServiceActivator(inputChannel = "eventBus")
public void collect(DomainEvent event) {
// meterRegistry.counter("events", "type", event.getType()).increment();
}
}
@Component
class CacheInvalidator {
@ServiceActivator(inputChannel = "eventBus")
public void invalidate(DomainEvent event) {
if ("PRODUCT_UPDATED".equals(event.getType())) {
// cacheManager.getCache("products").evict(event.getEntityId());
}
}
}