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

readCount

Items successfully returned by the ItemReader.

filterCount

Items the ItemProcessor filtered out by returning null — read, but deliberately not written.

writeCount

Items handed to the ItemWriter and committed.

commitCount

Transactions committed — one per chunk, so roughly readCount / chunkSize.

rollbackCount

Transactions rolled back, including those replayed after a skip.

readSkipCount

Items skipped because the reader threw a skippable exception.

processSkipCount

Items skipped because the processor threw a skippable exception.

writeSkipCount

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 (StepExecution.getExecutionContext())

One per StepExecution. Persisted at every commit point — that is, once per chunk. This is what makes restart work: a reader saves its position here, and on restart reads it back and resumes.

Job-scoped (JobExecution.getExecutionContext())

One per JobExecution. Persisted between steps. Use it to hand values from one step to the next.

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.

Further reading

For the full detail behind this page: