Steps, executions & the ExecutionContext
|
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. |
A Step is one independent, sequential phase of a job. Its runtime counterpart, the StepExecution, carries
the counters that tell you what actually happened, and the ExecutionContext is the small key/value store that
makes restart — and passing data between steps — possible.
Step
Every Job is made of `Step`s, and each step encapsulates one phase: stage a file, load it, aggregate it,
send a report. Steps come in two shapes — chunk-oriented steps
(Chunk-oriented processing) and tasklet steps
(Tasklet steps) — but both are built the same way:
@Bean
public Step archiveStep(JobRepository jobRepository, PlatformTransactionManager tx) {
return new StepBuilder("archiveStep", jobRepository)
.tasklet(new FileArchivingTasklet(), tx)
.build();
}
Keeping steps small is what makes a failure informative and a restart cheap: the job resumes at the first step that did not complete, so a five-step job that fails in step four repeats only step four.
StepExecution and its counters
A StepExecution is one attempt at one step within one JobExecution. Besides BatchStatus, ExitStatus,
timings and failureExceptions, it maintains the counters that are the primary operational signal of a batch
run:
| Counter | Meaning |
|---|---|
|
Items successfully returned by the |
|
Items the |
|
Items handed to the |
|
Transactions committed — one per chunk, so roughly |
|
Transactions rolled back, including those replayed after a skip. |
|
Items skipped because the reader threw a skippable exception. |
|
Items skipped because the processor threw a skippable exception. |
|
Items skipped because the writer threw a skippable exception. |
readCount = writeCount + filterCount + processSkipCount + writeSkipCount is the identity worth remembering
when reconciling a run. The skip counters only move in a fault-tolerant step — see
Fault tolerance: skip & retry.
Business code contributes to these counters through the StepContribution handed to a tasklet:
@Bean
public Step purgeStep(JobRepository jobRepository, PlatformTransactionManager tx, PurgeService purgeService) {
return new StepBuilder("purgeStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
int deleted = purgeService.purgeExpired();
contribution.incrementWriteCount(deleted);
return RepeatStatus.FINISHED;
}, tx)
.build();
}
The counters are persisted with the execution, so they are queryable long after the run — see The job repository & metadata schema.
The ExecutionContext
An ExecutionContext is a Map-like bag of key/value pairs that Spring Batch persists on the caller’s
behalf. There are two of them, with different lifetimes:
| Context | Lifetime and persistence |
|---|---|
Step-scoped ( |
One per |
Job-scoped ( |
One per |
Because both are written to the metadata tables, every stored value must be Serializable and should be
small. The context is not a cache: putting a collection of ten thousand IDs in it bloats every commit and
slows the whole step. Keep it to positions, counts and identifiers — and note the tuning consequence in
Profiling & tuning.
Reading and writing the context
Framework components use it automatically — FlatFileItemReader stores the line count, paging readers store
the page and offset — and application code can use it too, most simply from a listener or tasklet:
@Bean
public Step countingStep(JobRepository jobRepository, PlatformTransactionManager tx) {
return new StepBuilder("countingStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
ExecutionContext stepContext = chunkContext.getStepContext()
.getStepExecution()
.getExecutionContext();
long processed = stepContext.getLong("processed", 0L);
stepContext.putLong("processed", processed + 1);
return RepeatStatus.FINISHED;
}, tx)
.build();
}
A reader or writer that keeps its own state implements ItemStream so the framework calls open, update
and close around it:
public class WatermarkReader implements ItemStreamReader<Trade> {
private static final String KEY = "watermark.id";
private long lastId;
@Override
public void open(ExecutionContext executionContext) {
this.lastId = executionContext.getLong(KEY, 0L); // resume, or start at 0
}
@Override
public void update(ExecutionContext executionContext) {
executionContext.putLong(KEY, this.lastId); // called at each commit point
}
@Override
public Trade read() {
Trade next = tradeDao.findFirstAfter(lastId);
if (next != null) {
this.lastId = next.getId();
}
return next;
}
@Override
public void close() {
// release resources
}
}
Registering that stream with the step is described in Chunk-oriented processing.
Passing data between steps
Step contexts are not shared: step two cannot see step one’s step-scoped context. The supported way to hand
a value forward is to promote it to the job-scoped context with an ExecutionContextPromotionListener.
Step one puts the value in its own step context:
@Bean
public Step extractStep(JobRepository jobRepository, PlatformTransactionManager tx,
ExecutionContextPromotionListener promotionListener) {
return new StepBuilder("extractStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
String fileName = extractService.writeExtract();
chunkContext.getStepContext()
.getStepExecution()
.getExecutionContext()
.putString("extract.file", fileName);
return RepeatStatus.FINISHED;
}, tx)
.listener(promotionListener)
.build();
}
@Bean
public ExecutionContextPromotionListener promotionListener() {
ExecutionContextPromotionListener listener = new ExecutionContextPromotionListener();
listener.setKeys(new String[] { "extract.file" });
// optionally: only promote when the step ended with these exit codes
listener.setStatuses(new String[] { ExitStatus.COMPLETED.getExitCode() });
return listener;
}
The listener copies the named keys from the step context to the job context after the step completes. Step two then reads them back — most conveniently by late binding, since the job context is exposed to SpEL:
@Bean
@StepScope
public FlatFileItemReader<Trade> extractReader(
@Value("#{jobExecutionContext['extract.file']}") String fileName) {
return new FlatFileItemReaderBuilder<Trade>()
.name("extractReader")
.resource(new FileSystemResource(fileName))
.delimited()
.names("id", "isin", "quantity", "price")
.targetType(Trade.class)
.build();
}
Late binding with @StepScope and the jobExecutionContext / stepExecutionContext / jobParameters SpEL
roots is covered in Step flow & listeners. Note that
these expressions belong inside code, where the braces are literal; in ordinary prose an expression such as
#{jobExecutionContext['extract.file']} has to be written with the brace escaped.
For the pattern in the reference itself see Common batch patterns.