Configuring a job

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 Job bean is assembled with a JobBuilder: the steps in order, plus the cross-cutting configuration — restartability, parameter validation, an incrementer and listeners.

JobBuilder and step ordering

Since the removal of JobBuilderFactory, the builder is constructed directly with the job name and the JobRepository:

@Bean
public Job endOfDayJob(JobRepository jobRepository,
                       Step stageFileStep, Step loadStep, Step reportStep) {
    return new JobBuilder("endOfDayJob", jobRepository)
            .start(stageFileStep)
            .next(loadStep)
            .next(reportStep)
            .build();
}

.start(step) fixes the first step and returns a SimpleJobBuilder; .next(step) appends the next one. Steps run sequentially, and the job stops at the first step that fails.

.flow(…​) switches to the flow builder, which is what conditional transitions, splits and deciders need:

@Bean
public Job conditionalJob(JobRepository jobRepository, Flow mainFlow) {
    return new JobBuilder("conditionalJob", jobRepository)
            .start(mainFlow)
            .end()
            .build();
}

Everything that can be expressed inside a flow — .on(…​), .to(…​), .from(…​), splits, deciders — is covered in Step flow & listeners.

Restartability and start limits

By default a failed or stopped JobInstance can be restarted, and completed steps are skipped on the retry. Two switches change that.

.preventRestart() marks the job non-restartable: if an execution fails, that JobInstance is finished for good, and attempting to launch it again throws JobRestartException. Use it when a partial run leaves the system in a state that only a fresh instance can safely process:

@Bean
public Job oneShotJob(JobRepository jobRepository, Step transferStep) {
    return new JobBuilder("oneShotJob", jobRepository)
            .preventRestart()
            .start(transferStep)
            .build();
}

At the step level, startLimit caps how many times a given step may be started across all executions of one instance — a guard against a step being retried indefinitely by repeated restarts — and allowStartIfComplete(true) forces a step to run again on restart even though it already completed (useful for validation or clean-up steps that must always run):

@Bean
public Step validateStep(JobRepository jobRepository, PlatformTransactionManager tx) {
    return new StepBuilder("validateStep", jobRepository)
            .tasklet(new ValidateInputTasklet(), tx)
            .startLimit(3)              // at most 3 attempts across restarts
            .allowStartIfComplete(true) // re-run even if it completed previously
            .build();
}

The restart semantics themselves — what is skipped, what is replayed — are in Stopping, restart & recovery.

Validating parameters

A job that requires a business date should refuse to start without one, rather than failing three steps in. DefaultJobParametersValidator takes the required and optional key names:

@Bean
public JobParametersValidator endOfDayValidator() {
    DefaultJobParametersValidator validator = new DefaultJobParametersValidator(
            new String[] { "run.date" },                    // required keys
            new String[] { "output.dir", "dry.run" });      // optional keys
    validator.afterPropertiesSet();
    return validator;
}

@Bean
public Job endOfDayJob(JobRepository jobRepository, Step loadStep,
                       JobParametersValidator endOfDayValidator) {
    return new JobBuilder("endOfDayJob", jobRepository)
            .validator(endOfDayValidator)
            .start(loadStep)
            .build();
}

Any key that is neither required nor optional makes the launch fail — which catches typos in scheduler scripts before they cost a night.

For rules the default validator cannot express — a value range, a relationship between two parameters, a readable file — implement JobParametersValidator:

public class RunDateValidator implements JobParametersValidator {

    @Override
    public void validate(JobParameters parameters) throws JobParametersInvalidException {
        LocalDate runDate = parameters.getLocalDate("run.date");
        if (runDate == null) {
            throw new JobParametersInvalidException("run.date is required");
        }
        if (runDate.isAfter(LocalDate.now())) {
            throw new JobParametersInvalidException(
                    "run.date must not be in the future, was " + runDate);
        }
        if (runDate.getDayOfWeek() == DayOfWeek.SUNDAY) {
            throw new JobParametersInvalidException("no settlement run on Sundays");
        }
    }
}

Several validators can be combined with CompositeJobParametersValidator.

Incrementers

A JobParametersIncrementer computes the next parameter set from the previous one, so an operator can say "run the next instance" without composing parameters by hand. RunIdIncrementer bumps a run.id counter:

@Bean
public Job nightlyJob(JobRepository jobRepository, Step loadStep) {
    return new JobBuilder("nightlyJob", jobRepository)
            .incrementer(new RunIdIncrementer())
            .start(loadStep)
            .build();
}

A date-based incrementer is often more meaningful, because the resulting instance identity carries business sense:

public class NextBusinessDateIncrementer implements JobParametersIncrementer {

    @Override
    public JobParameters getNext(JobParameters parameters) {
        LocalDate previous = (parameters == null) ? null : parameters.getLocalDate("run.date");
        LocalDate next = (previous == null) ? LocalDate.now() : previous.plusDays(1);
        while (next.getDayOfWeek() == DayOfWeek.SATURDAY || next.getDayOfWeek() == DayOfWeek.SUNDAY) {
            next = next.plusDays(1);
        }
        return new JobParametersBuilder(parameters == null ? new JobParameters() : parameters)
                .addLocalDate("run.date", next)
                .toJobParameters();
    }
}

The incrementer is applied by JobOperator.startNextInstance(job) — see Running a job. It is not applied by a plain start(job, parameters), which uses exactly the parameters given.

JobExecutionListener

Job-level listeners run before the first step and after the last one, regardless of outcome. Implement the interface, or use the annotations:

@Component
public class JobTimingListener {

    private static final Logger log = LoggerFactory.getLogger(JobTimingListener.class);

    @BeforeJob
    public void before(JobExecution jobExecution) {
        log.info("starting {} execution {} with {}",
                jobExecution.getJobInstance().getJobName(),
                jobExecution.getId(),
                jobExecution.getJobParameters());
    }

    @AfterJob
    public void after(JobExecution jobExecution) {
        Duration duration = Duration.between(jobExecution.getStartTime(), jobExecution.getEndTime());
        log.info("{} finished as {} in {}s",
                jobExecution.getJobInstance().getJobName(),
                jobExecution.getStatus(),
                duration.toSeconds());
        if (jobExecution.getStatus() == BatchStatus.FAILED) {
            jobExecution.getAllFailureExceptions()
                    .forEach(throwable -> log.error("failure", throwable));
        }
    }
}
@Bean
public Job endOfDayJob(JobRepository jobRepository, Step loadStep, JobTimingListener timingListener) {
    return new JobBuilder("endOfDayJob", jobRepository)
            .listener(timingListener)
            .start(loadStep)
            .build();
}

@AfterJob runs even when the job failed, which makes it the right place for notifications and clean-up. Step-level and item-level listeners are covered in Step flow & listeners.

Under Spring Boot, the concrete auto-configuration around all of this — the repository, the datasource, the startup runner — is described on Spring Batch (SpringBoot Reference).

Further reading

For the full detail behind this page: