Chunk-oriented processing
|
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. |
Chunk-oriented processing is what Spring Batch is for: read items one at a time, transform each one, accumulate them until a chunk is full, then write the whole chunk inside a single transaction.
The loop
one item"] Read -->|null| Flush["write the partial chunk,
commit, end the step"] Read -->|item| Process["ItemProcessor.process(item)
transform or filter"] Process -->|null: filtered| Full Process -->|output| Buffer["add to the in-memory chunk"] Buffer --> Full{"chunk complete?
(commit interval reached)"} Full -->|no| Read Full -->|yes| Write["ItemWriter.write(chunk)
the whole list at once"] Write --> Update["update the StepExecution,
persist the ExecutionContext"] Update --> Commit["commit transaction"] Commit --> Tx Flush --> Done(["step ends"])
Three properties of that picture matter:
-
the reader and processor see one item at a time, so memory is bounded by the chunk, not by the input;
-
the writer sees the whole chunk, so it can issue one batched statement instead of N;
-
the commit interval is the chunk size is the transaction boundary — these are three names for one number. A chunk of 500 means one transaction per 500 items, one
ExecutionContextsave per 500 items, and a restart granularity of 500 items.
If anything in the chunk fails, the whole transaction rolls back — all 500 items — and the framework’s fault-tolerance machinery decides whether to retry, skip or fail (Fault tolerance: skip & retry).
Building a chunk step
The current idiom constructs the builder directly and passes the chunk size and the transaction manager:
@Bean
public Step loadTradesStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
ItemReader<TradeCsv> reader,
ItemProcessor<TradeCsv, Trade> processor,
ItemWriter<Trade> writer) {
return new StepBuilder("loadTradesStep", jobRepository)
.<TradeCsv, Trade>chunk(500, transactionManager)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
The two type parameters are the input type (what the reader returns) and the output type (what the processor produces and the writer consumes). They may differ — that type change is the processor’s main job. A step without a processor uses the same type twice.
The 6.0 ChunkOrientedStep
Spring Batch 6.0 introduces ChunkOrientedStep and its ChunkOrientedStepBuilder, which take the chunk size
as a constructor argument and expose the new concurrency model (see
Scaling & parallel processing):
@Bean
public Step loadTradesStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
ItemReader<TradeCsv> reader,
ItemProcessor<TradeCsv, Trade> processor,
ItemWriter<Trade> writer) {
return new ChunkOrientedStepBuilder<TradeCsv, Trade>("loadTradesStep", jobRepository, 500)
.reader(reader)
.processor(processor)
.writer(writer)
.transactionManager(transactionManager)
.build();
}
The two forms are not equal in status. StepBuilder.chunk(int, PlatformTransactionManager) and
chunk(CompletionPolicy, PlatformTransactionManager) — along with the SimpleStepBuilder and
FaultTolerantStepBuilder they return — are annotated @Deprecated(since = "6.0", forRemoval = true). The
non-deprecated entry point is StepBuilder.chunk(int), which returns a ChunkOrientedStepBuilder.
The .chunk(size, transactionManager) form is still what most existing code and most published examples use,
and it is what the other pages in this section show, so it is worth recognising — but new code should prefer
ChunkOrientedStepBuilder, and code being migrated should plan to move off the deprecated form.
Dynamic chunk sizes with a CompletionPolicy
A fixed chunk size is a fixed count. When "how much is a chunk" depends on something else — elapsed time, a
change of business key, a memory budget — supply a CompletionPolicy instead:
@Bean
public Step timeBoundedStep(JobRepository jobRepository, PlatformTransactionManager tx,
ItemReader<Trade> reader, ItemWriter<Trade> writer) {
CompositeCompletionPolicy policy = new CompositeCompletionPolicy();
policy.setPolicies(new CompletionPolicy[] {
new SimpleCompletionPolicy(1000), // at most 1000 items ...
new TimeoutTerminationPolicy(5_000L) // ... or 5 seconds, whichever comes first
});
return new StepBuilder("timeBoundedStep", jobRepository)
.<Trade, Trade>chunk(policy, tx)
.reader(reader)
.writer(writer)
.build();
}
SimpleCompletionPolicy is what a plain integer chunk size creates internally. A custom policy implements
CompletionPolicy and decides, per item, whether the chunk is finished — for instance closing the chunk
whenever the account number changes, so that all items for one account commit together.
The underlying RepeatOperations abstraction that runs this loop is described in
Repeat & retry internals.
Transaction attributes
The chunk transaction is a normal Spring transaction, and its attributes are configurable per step:
@Bean
public Step isolatedStep(JobRepository jobRepository, PlatformTransactionManager tx,
ItemReader<Trade> reader, ItemWriter<Trade> writer) {
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
attribute.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
attribute.setIsolationLevel(Isolation.READ_COMMITTED.value());
attribute.setTimeout(120); // seconds; fail the chunk rather than hold locks forever
return new StepBuilder("isolatedStep", jobRepository)
.<Trade, Trade>chunk(200, tx)
.reader(reader)
.writer(writer)
.transactionAttribute(attribute)
.build();
}
-
Propagation —
REQUIREDby default.REQUIRES_NEWisolates the chunk from any surrounding transaction;NOT_SUPPORTEDruns the chunk without one, which only makes sense for a read-only step. -
Isolation — the default is the database’s. Raising it increases contention; see SQL Transactions.
-
Timeout — an upper bound on one chunk. A long timeout plus a large chunk is the classic recipe for lock contention with the online system.
.noRollback(SomeException.class) declares exceptions that must not roll the chunk back — typically a
business exception thrown by a writer that has already handled the failure itself:
return new StepBuilder("tolerantStep", jobRepository)
.<Trade, Trade>chunk(200, tx)
.reader(reader)
.writer(writer)
.faultTolerant()
.noRollback(DuplicateTradeException.class)
.build();
Registering an ItemStream
Readers and writers that hold restart state implement ItemStream, and the step must know about them so that
open, update and close are called at the right moments. A reader or writer passed to .reader(…) or
.writer(…) is registered automatically — but a delegate hidden inside a composite or a custom component
is not, and must be registered explicitly:
@Bean
public Step compositeWriterStep(JobRepository jobRepository, PlatformTransactionManager tx,
ItemReader<Trade> reader,
FlatFileItemWriter<Trade> auditFileWriter,
JdbcBatchItemWriter<Trade> databaseWriter) {
CompositeItemWriter<Trade> composite = new CompositeItemWriter<>();
composite.setDelegates(List.of(databaseWriter, auditFileWriter));
return new StepBuilder("compositeWriterStep", jobRepository)
.<Trade, Trade>chunk(500, tx)
.reader(reader)
.writer(composite)
.stream(auditFileWriter) // the delegate needs open/update/close
.build();
}
Forgetting the .stream(…) registration is the usual cause of "the file was never created" or "restart
started from the beginning of the file".