Spring Batch Integration

This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

spring-batch-integration connects batch jobs to messaging. It covers three distinct needs: starting a job because a message arrived, overlapping slow processing with reading, and distributing work across JVMs.

Launching a job from a message

A file lands on a share, an event arrives on a queue — and a job should start. JobLaunchRequest packages a Job and its JobParameters as a message payload; JobLaunchingMessageHandler (or the JobLaunchingGateway wrapper) executes it.

The usual shape is a transformer that turns the inbound payload into a request, then the gateway:

@Component
public class FileToJobLaunchRequestTransformer {

    private final Job importJob;

    public FileToJobLaunchRequestTransformer(Job importJob) {
        this.importJob = importJob;
    }

    @Transformer(inputChannel = "incomingFiles", outputChannel = "jobRequests")
    public JobLaunchRequest toRequest(Message<File> message) {
        File file = message.getPayload();
        JobParameters parameters = new JobParametersBuilder()
                .addString("input.file", file.getAbsolutePath())
                .addLocalDateTime("arrival.time", LocalDateTime.now())
                .toJobParameters();
        return new JobLaunchRequest(importJob, parameters);
    }
}
@Configuration
public class JobLaunchingFlowConfiguration {

    @Bean
    @ServiceActivator(inputChannel = "jobRequests", outputChannel = "jobExecutions")
    public JobLaunchingGateway jobLaunchingGateway(JobOperator jobOperator) {
        return new JobLaunchingGateway(jobOperator);
    }

    @Bean
    public IntegrationFlow incomingFileFlow() {
        return IntegrationFlow
                .from(Files.inboundAdapter(new File("/data/in"))
                                .patternFilter("trades-*.csv"),
                        poller -> poller.poller(Pollers.fixedDelay(Duration.ofSeconds(10))))
                .channel("incomingFiles")
                .get();
    }
}

The gateway’s reply is the resulting JobExecution, so a downstream endpoint can report on the outcome — informational feedback without polling the repository:

@ServiceActivator(inputChannel = "jobExecutions")
public void report(JobExecution execution) {
    log.info("{} #{} finished as {}",
            execution.getJobInstance().getJobName(),
            execution.getId(),
            execution.getStatus());
    if (execution.getStatus() == BatchStatus.FAILED) {
        alertService.raise(execution);
    }
}

Whether the gateway blocks depends on the JobOperator’s `TaskExecutor — synchronous means the reply carries a finished execution, asynchronous means it carries one that has only just started. See Running a job.

Asynchronous processing inside a step

AsyncItemProcessor submits each process() call to a TaskExecutor and returns a Future; AsyncItemWriter collects the futures and passes the resolved items to its delegate. Together they overlap a slow, I/O-bound processor with reading and writing:

@Bean
public AsyncItemProcessor<Trade, EnrichedTrade> asyncEnrichingProcessor(
        ItemProcessor<Trade, EnrichedTrade> delegate, TaskExecutor taskExecutor) {
    AsyncItemProcessor<Trade, EnrichedTrade> processor = new AsyncItemProcessor<>();
    processor.setDelegate(delegate);           // calls a slow pricing service
    processor.setTaskExecutor(taskExecutor);
    return processor;
}

@Bean
public AsyncItemWriter<EnrichedTrade> asyncEnrichedWriter(ItemWriter<EnrichedTrade> delegate) {
    AsyncItemWriter<EnrichedTrade> writer = new AsyncItemWriter<>();
    writer.setDelegate(delegate);
    return writer;
}

The step’s output type becomes Future<EnrichedTrade>, as shown in Scaling & parallel processing. The delegate processor must be thread-safe, and it still must be idempotent (ItemProcessors).

SEDA-style stage decoupling

BlockingQueueItemWriter and BlockingQueueItemReader let one step’s output become another step’s input through an in-memory queue, so producer and consumer run concurrently at their own paces — the staged event-driven architecture idea applied to a job:

@Bean
public BlockingQueue<Trade> stagingQueue() {
    return new LinkedBlockingQueue<>(10_000);   // bounded: back-pressure on the producer
}

@Bean
public BlockingQueueItemWriter<Trade> queueWriter(BlockingQueue<Trade> stagingQueue) {
    return new BlockingQueueItemWriter<>(stagingQueue);
}

@Bean
public BlockingQueueItemReader<Trade> queueReader(BlockingQueue<Trade> stagingQueue) {
    BlockingQueueItemReader<Trade> reader = new BlockingQueueItemReader<>(stagingQueue);
    reader.setTimeout(5, TimeUnit.SECONDS);   // how long to wait before deciding the queue is exhausted
    return reader;
}

Bound the queue: an unbounded one turns a fast producer into an out-of-memory error. And note the trade-off — items in the queue are in memory only, so a crash loses them; the two stages are not restartable as one.

Remote chunking

The manager reads and dispatches; workers process and write. On the manager, the writer is a ChunkMessageChannelItemWriter that sends the chunk to a request channel and correlates the reply.

Both this section and the remote-partitioning one below rely on @EnableBatchIntegration: it is what registers RemoteChunkingManagerStepBuilderFactory, RemotePartitioningManagerStepBuilderFactory and RemotePartitioningWorkerStepBuilderFactory as beans, so without it these configurations fail to autowire. These builder factories belong to spring-batch-integration and are distinct from the core JobBuilderFactory/StepBuilderFactory that Getting started lists as removed — the factory.get("name") idiom below is the current one for remote steps.

@Configuration
@EnableBatchIntegration          // registers the remote step builder factories
public class RemoteChunkingManagerConfiguration {

    @Bean
    public DirectChannel chunkRequests() {
        return new DirectChannel();
    }

    @Bean
    public QueueChannel chunkReplies() {
        return new QueueChannel();
    }

    @Bean
    public IntegrationFlow outboundChunks(ConnectionFactory connectionFactory) {
        return IntegrationFlow.from(chunkRequests())
                .handle(Amqp.outboundAdapter(new RabbitTemplate(connectionFactory))
                        .routingKey("batch.chunk.requests"))
                .get();
    }

    @Bean
    public IntegrationFlow inboundReplies(ConnectionFactory connectionFactory) {
        return IntegrationFlow
                .from(Amqp.inboundAdapter(connectionFactory, "batch.chunk.replies"))
                .channel(chunkReplies())
                .get();
    }

    @Bean
    public Step managerStep(RemoteChunkingManagerStepBuilderFactory managerStepBuilderFactory,
                            ItemReader<Trade> reader) {
        return managerStepBuilderFactory.get("managerStep")
                .<Trade, Trade>chunk(500)
                .reader(reader)
                .outputChannel(chunkRequests())
                .inputChannel(chunkReplies())
                .build();
    }
}

On the worker, a ChunkProcessorChunkHandler applies the processor and writer and replies:

@Configuration
@EnableBatchIntegration
public class RemoteChunkingWorkerConfiguration {

    @Bean
    public IntegrationFlow workerFlow(RemoteChunkingWorkerBuilder<Trade, Trade> workerBuilder,
                                      ItemProcessor<Trade, Trade> processor,
                                      ItemWriter<Trade> writer,
                                      ConnectionFactory connectionFactory) {
        return workerBuilder
                .itemProcessor(processor)
                .itemWriter(writer)
                .inputChannel(workerRequests(connectionFactory))
                .outputChannel(workerReplies(connectionFactory))
                .build();
    }
}

Requirements worth repeating: the middleware must be durable and transactional, and items must be serialisable. The topology and its trade-offs are pictured in Scaling & parallel processing.

Remote partitioning

Remote partitioning sends step execution requests, not chunks — each worker reads its own slice. The manager uses a MessageChannelPartitionHandler; workers run a StepExecutionRequestHandler against the shared JobRepository:

// inside a @Configuration @EnableBatchIntegration class
@Bean
public Step remotePartitionedStep(
        RemotePartitioningManagerStepBuilderFactory managerStepBuilderFactory,
        Partitioner tradeIdRangePartitioner,
        DirectChannel partitionRequests,
        QueueChannel partitionReplies) {

    return managerStepBuilderFactory.get("remotePartitionedStep")
            .partitioner("workerStep", tradeIdRangePartitioner)
            .gridSize(8)
            .outputChannel(partitionRequests)
            .inputChannel(partitionReplies)     // or .pollInterval(...) for polling the repository
            .build();
}
// inside a @Configuration @EnableBatchIntegration class
@Bean
public Step workerStep(RemotePartitioningWorkerStepBuilderFactory workerStepBuilderFactory,
                       ItemReader<Trade> partitionedReader,
                       ItemWriter<Trade> writer,
                       DirectChannel workerRequests) {
    return workerStepBuilderFactory.get("workerStep")
            .inputChannel(workerRequests)
            .<Trade, Trade>chunk(500)
            .reader(partitionedReader)          // @StepScope, bound to its slice
            .writer(writer)
            .build();
}

Because each partition is a real, persisted StepExecution, remote partitioning keeps normal restart semantics: only the partitions that failed are rerun.

Writers that publish to a channel, a JMS destination or an SMTP server also live here, which is where the messaging and mail destinations mentioned in ItemWriters: databases & alternative destinations come from.

Further reading

For the full detail behind this page: