Skip to content
Spring Boot sb batch 4 min read

Jobs & Steps

A Job is the unit you launch; a Step is a phase within it. Spring Batch 5 builds both with fluent builders that take the JobRepository directly — JobBuilder and StepBuilder — and lets you wire steps into linear sequences or branching flows based on each step’s exit status. This page covers defining steps of both kinds and assembling them into a job. If you are new to the model, start with the Batch introduction.

Defining a Step

Every step is created from a StepBuilder with a unique name and the shared JobRepository. From there you choose its type.

Chunk-oriented steps

A chunk step reads items, optionally processes them, and writes them in batches. .chunk(n, txManager) sets the commit interval — after every n items the writer flushes and the transaction commits.

import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.transaction.PlatformTransactionManager;

@Bean
Step importStep(JobRepository jobRepository,
                PlatformTransactionManager txManager,
                ItemReader<Customer> reader,
                ItemProcessor<Customer, Customer> processor,
                ItemWriter<Customer> writer) {

    return new StepBuilder("importStep", jobRepository)
            .<Customer, Customer>chunk(500, txManager)   // <input, output>
            .reader(reader)
            .processor(processor)   // optional
            .writer(writer)
            .build();
}

The two type parameters on .chunk() are the input type (what the reader produces) and the output type (what the processor returns and the writer consumes). They differ when a processor maps one type to another — e.g. <CustomerCsv, CustomerEntity>.

Tip: The chunk size is a throughput knob, not a correctness one. Larger chunks mean fewer commits and faster runs but more memory and bigger rollbacks on failure. Start around 100–1000 and tune against your data. See Reader, Processor, Writer for the components themselves.

Tasklet steps

A tasklet runs a single block of logic and returns RepeatStatus.FINISHED. It’s ideal for setup/teardown — truncating a staging table, archiving a file, or calling a procedure.

import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.jdbc.core.JdbcTemplate;

@Bean
Step truncateStagingStep(JobRepository jobRepository,
                         PlatformTransactionManager txManager,
                         JdbcTemplate jdbc) {
    return new StepBuilder("truncateStaging", jobRepository)
            .tasklet((contribution, chunkContext) -> {
                jdbc.update("TRUNCATE TABLE customer_staging");
                return RepeatStatus.FINISHED;
            }, txManager)
            .build();
}

Return RepeatStatus.CONTINUABLE instead of FINISHED to have the tasklet invoked again in a fresh transaction — useful for paged deletes that should commit in batches rather than one huge transaction.

Defining a Job

A Job strings steps together. The simplest job has one step; .start(...) begins the flow and .next(...) chains the rest.

import org.springframework.batch.core.Job;
import org.springframework.batch.core.job.builder.JobBuilder;

@Bean
Job customerImportJob(JobRepository jobRepository,
                      Step truncateStagingStep,
                      Step importStep) {
    return new JobBuilder("customerImportJob", jobRepository)
            .start(truncateStagingStep)
            .next(importStep)
            .build();
}

Steps run in order, and by default the job stops the moment a step fails. Each step commits independently, so on restart Spring Batch skips the steps that already completed and resumes at the one that failed.

Multi-step flows

Most real jobs are a small pipeline: prepare → process → finalize. Chain them with .next().

@Bean
Job nightlyJob(JobRepository jobRepository,
               Step extractStep, Step transformStep,
               Step loadStep, Step notifyStep) {
    return new JobBuilder("nightlyJob", jobRepository)
            .start(extractStep)
            .next(transformStep)
            .next(loadStep)
            .next(notifyStep)
            .build();
}
MethodMeaning
.start(step)the first step (or the start of a flow)
.next(step)the next step in a linear sequence
.on(exitCode)begin a conditional transition based on a step’s exit status
.to(step)the target of a transition
.from(step)add another transition originating from a step
.end() / .fail() / .stop()terminate the flow with a job status

Conditional flow and transitions

Linear chains stop at the first failure. When you need branching — run a recovery step on failure, skip a step when there’s nothing to do — use the transition API driven by each step’s exit status.

@Bean
Job conditionalJob(JobRepository jobRepository,
                   Step processStep, Step cleanupStep, Step alertStep) {
    return new JobBuilder("conditionalJob", jobRepository)
            .start(processStep)
                .on("FAILED").to(alertStep)        // on failure, alert and stop
            .from(processStep)
                .on("*").to(cleanupStep)           // otherwise, clean up
            .end()
            .build();
}

on(...) matches the step’s ExitStatus code (a String) with * (any chars) and ? (one char) wildcards. Don’t confuse it with BatchStatus, the enum that records overall outcome (COMPLETED, FAILED, STOPPED). A step’s processing logic, or a StepExecutionListener, can set a custom exit status to steer the flow:

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;

public class SkipIfEmptyListener implements StepExecutionListener {
    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        return stepExecution.getReadCount() == 0
                ? new ExitStatus("NOTHING_TO_DO")
                : stepExecution.getExitStatus();
    }
}
return new JobBuilder("smartJob", jobRepository)
        .start(loadStep).listener(new SkipIfEmptyListener())
            .on("NOTHING_TO_DO").end()             // finish early, mark COMPLETED
        .from(loadStep)
            .on("*").to(reportStep)
        .end()
        .build();

Warning: .end() finishes the flow as COMPLETED; .fail() finishes as FAILED (the instance can be rerun); .stop() pauses as STOPPED so an operator can resume it. Always provide a .from(step).on("*") catch-all branch — an unmatched exit status throws an exception and the flow has nowhere to go.

Restarting and idempotency

Because the JobRepository records every StepExecution, a failed job restarted with the same identifying parameters resumes at the failed step. Steps that already completed are not re-run by default. Make your tasklets and writers idempotent (e.g. upserts, truncate-before-load) so a partial run followed by a restart doesn’t double-write. Parameters and restart are covered in Running & Scheduling Jobs.

Last updated June 13, 2026
Was this helpful?