Jobs, instances & parameters

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.

Four types carry the whole job-side vocabulary of Spring Batch: Job, JobInstance, JobParameters and JobExecution. Getting the relationship between them right is what makes restart behave the way you expect.

Job

A Job is a container for an ordered sequence of `Step`s plus the configuration that applies to the run as a whole — restartability, a parameter validator, an incrementer, and job-level listeners. It is a definition, a singleton bean, and it holds no runtime state:

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

The job’s name is its identity in the JobRepository; renaming a job starts its history over.

JobInstance: a job plus its identifying parameters

A JobInstance is a logical run of a job: the combination of the job name and its identifying JobParameters. "The end-of-day job for 2026-01-31" is one JobInstance. Running it again on the same date resumes that instance; running it with 2026-02-01 creates a new one.

This is the rule that surprises newcomers: Spring Batch refuses to start a JobInstance that has already completed. A completed instance cannot be rerun — only a failed or stopped one can be restarted. If the same logical work must genuinely run twice, it needs different identifying parameters (which is exactly what a JobParametersIncrementer provides, below).

JobParameters

JobParameters is a typed map passed in at launch. Each JobParameter carries a value, its Java type, and an identifying flag:

JobParameters parameters = new JobParametersBuilder()
        .addLocalDate("run.date", LocalDate.of(2026, 1, 31))   // identifying by default
        .addLong("run.id", 1L)                                  // identifying by default
        .addString("output.dir", "/tmp/out", false)             // false => NOT identifying
        .toJobParameters();
  • Typed: addString, addLong, addDouble, addDate, addLocalDate, addLocalDateTime, and the generic addJobParameter(key, value, Class<T>, identifying). Types are preserved in the metadata tables, so a parameter read back later is still a LocalDate, not a string.

  • Identifying (the default) means the parameter takes part in deciding which JobInstance this is. Change it and you get a new instance — "start fresh".

  • Non-identifying parameters are recorded and available to the job but do not affect instance identity. Output directories, verbosity flags and retry limits belong here, so that changing one on a restart does not accidentally create a new instance.

That distinction is precisely the difference between the two things an operator might mean by "run it again":

Intent What to do

Resume where you left off — the previous run failed halfway

Launch with the same identifying parameters. Spring Batch finds the existing JobInstance, creates a new JobExecution for it, and steps that already completed are skipped (see Stopping, restart & recovery).

Start fresh — process a new period, or reprocess from scratch

Launch with different identifying parameters (a new business date, or a new run.id from an incrementer). A brand-new JobInstance is created with an empty ExecutionContext.

Parameters are read inside a step through late binding, which is covered in Step flow & listeners, and validated as described in Configuring a job.

JobExecution

A JobExecution is one attempt at running a JobInstance. One instance may have many executions — one per restart. It records:

  • BatchStatus — the framework’s own enum for where the execution stands;

  • ExitStatus — the outcome as a code plus description, used by flow transitions and by callers such as a shell script;

  • startTime, endTime, createTime, lastUpdated;

  • failureExceptions — the throwables that ended it;

  • the job-level ExecutionContext (Steps, executions & the ExecutionContext);

  • the `StepExecution`s it produced.

BatchStatus lifecycle

stateDiagram-v2 [*] --> STARTING STARTING --> STARTED STARTED --> COMPLETED : all steps completed STARTED --> FAILED : unhandled exception STARTED --> STOPPING : stop() requested STOPPING --> STOPPED STOPPED --> STARTING : restart FAILED --> STARTING : restart FAILED --> ABANDONED : abandon() STOPPED --> ABANDONED : abandon() COMPLETED --> [*] ABANDONED --> [*]

UNKNOWN is the remaining value: an execution whose fate could not be determined, typically after a process crash. Recovering those is what the 6.0 JobOperator.recover() is for — see Stopping, restart & recovery.

BatchStatus vs. ExitStatus

BatchStatus is a fixed enum owned by the framework. ExitStatus is an open, extensible pair of an exit code (a String) and a description; a step or a listener may set any code it likes, and flow transitions match on it:

@Bean
public StepExecutionListener classifyingListener() {
    return new StepExecutionListener() {
        @Override
        public ExitStatus afterStep(StepExecution stepExecution) {
            if (stepExecution.getWriteCount() == 0) {
                return new ExitStatus("NO_DATA", "the input contained no rows to write");
            }
            return stepExecution.getExitStatus();   // leave COMPLETED as-is
        }
    };
}

.on("NO_DATA") in a job flow then routes on that code, as shown in Step flow & listeners.

Two executions of one instance

The following job is deliberately fragile: it fails the first time and succeeds the second, so the same JobInstance accumulates two `JobExecution`s.

@Configuration
public class RestartDemoConfiguration {

    @Bean
    public Step flakyStep(JobRepository jobRepository, PlatformTransactionManager tx) {
        return new StepBuilder("flakyStep", jobRepository)
                .tasklet((contribution, chunkContext) -> {
                    Path marker = Path.of("target", "already-ran");
                    if (!Files.exists(marker)) {
                        Files.createDirectories(marker.getParent());
                        Files.createFile(marker);
                        throw new IllegalStateException("first attempt always fails");
                    }
                    return RepeatStatus.FINISHED;
                }, tx)
                .build();
    }

    @Bean
    public Job restartDemoJob(JobRepository jobRepository, Step flakyStep) {
        return new JobBuilder("restartDemoJob", jobRepository)
                .validator(new DefaultJobParametersValidator(
                        new String[] { "run.date" },      // required
                        new String[] { "output.dir" }))   // optional
                .start(flakyStep)
                .build();
    }
}

Launching it twice with the same identifying parameters produces two executions of one instance:

JobParameters parameters = new JobParametersBuilder()
        .addLocalDate("run.date", LocalDate.of(2026, 1, 31))
        .toJobParameters();

JobExecution first  = jobOperator.start(restartDemoJob, parameters);   // FAILED
JobExecution second = jobOperator.start(restartDemoJob, parameters);   // COMPLETED

assert first.getJobInstance().equals(second.getJobInstance());

Forcing a new instance with an incrementer

When a job should be runnable repeatedly with otherwise identical parameters — a nightly job whose only distinguishing feature is "another run" — attach a JobParametersIncrementer. RunIdIncrementer adds (or bumps) a run.id parameter, so every launch through startNextInstance gets a fresh JobInstance:

@Bean
public Job nightlyJob(JobRepository jobRepository, Step flakyStep) {
    return new JobBuilder("nightlyJob", jobRepository)
            .incrementer(new RunIdIncrementer())
            .start(flakyStep)
            .build();
}
// each call creates the NEXT JobInstance rather than restarting the last one
jobOperator.startNextInstance(nightlyJob);   // the Job bean, not its name

Custom incrementers — for example one that advances a business date — are shown in Configuring a job.

Further reading

For the full detail behind this page: