Skip to main content

Spring Batch 5 to 6 Migration Guide

Spring Batch 6 moves classes to new packages, changes what the Spring Boot starter gives you, and adds a second way to write a chunk step. Most of it fails loudly. One change does not: a listener that stops being called. This article migrates one small job from Batch 5 to Batch 6, one step at a time, and shows what each step prints. You should know what a batch job and step are. Everything else is explained on the way. If you are new to Batch itself, start with Spring Batch on Boot 4.1: jobs, steps, chunk processing and restartability.
Versions tested. Before: Spring Boot 3.5.16 with Spring Batch 5.2.6 on JDK 21. After: Spring Boot 4.1.1 with Spring Batch 6.0.5, the latest GA, on JDK 25.0.4.1. Batch 6.1.0-M1 and 6.1.0-M2 exist as milestones and were not tested. Date: 28 September 2026. Companion code: spring-boot-demo/batch-5-to-6.

The starting point: a Batch 5 job that works

The job reads seven numbers, squares them, and writes them in chunks of three. It has a chunk listener that prints progress, a runner that starts the job and asks how many job instances exist, and one test. This is the configuration:
    @Bean
    ChunkListener chunkListener() {
        return new ChunkListener() {
            @Override public void afterChunk(ChunkContext context) {
                System.out.println("CHUNK DONE, read so far=" + context.getStepContext().getStepExecution().getReadCount());
            }
        };
    }

    @Bean
    Step squareStep(JobRepository repo, PlatformTransactionManager tx, ChunkListener listener) {
        return new StepBuilder("squareStep", repo)
                .<Integer, String>chunk(3, tx)
                .reader(reader())
                .processor(i -> "sq" + (i * i))
                .writer(writer())
                .listener(listener)
                .build();
    }

    @Bean
    Job squareJob(JobRepository repo, Step squareStep) {
        return new JobBuilder("squareJob", repo).start(squareStep).build();
    }
}

Source: BatchConfig.java, lines 31–55.

    private final JobLauncher launcher;
    private final JobExplorer explorer;
    private final Job job;

    public JobRunner(JobLauncher launcher, JobExplorer explorer, Job squareJob) {
        this.launcher = launcher; this.explorer = explorer; this.job = squareJob;
    }

    public void run() {
        try {
            JobParameters params = new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
            JobExecution exec = launcher.run(job, params);
            System.out.println("STATUS " + exec.getStatus() + " exit=" + exec.getExitStatus().getExitCode());
            System.out.println("INSTANCES " + explorer.getJobInstanceCount("squareJob"));
            System.out.println("DONE " + (exec.getStatus() == BatchStatus.COMPLETED));
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

Source: JobRunner.java, lines 14–32.

$ mvn test   # Boot 3.5.16, Batch 5.2.6, JDK 21
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.750 s -- in com.ankurm.batch.JobTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

$ java -jar target/batch-legacy-1.0.0.jar
WRITE [sq1, sq4, sq9]
CHUNK DONE, read so far=3
WRITE [sq16, sq25, sq36]
CHUNK DONE, read so far=6
WRITE [sq49]
CHUNK DONE, read so far=7
STATUS COMPLETED exit=COMPLETED
INSTANCES 1
DONE true

Captured in 01-legacy-batch5.txt.

Step 1: bump Boot and count what breaks

Change only the Boot version to 4.1.1, which brings Batch 6.0.5, and compile. Nothing in the source has changed yet:
$ mvn compile   # legacy sources, only the Boot version changed to 4.1.1 (Batch 6.0.5)
compile error lines reported: 56

packages that no longer exist:
package org.springframework.batch.core.explore does not exist
package org.springframework.batch.item does not exist
package org.springframework.batch.item.support does not exist

classes that moved:
ChunkListener ItemReader ItemWriter Job JobExecution JobExplorer JobParameters JobParametersBuilder ListItemReader Step 

Captured in 02-compile-errors-after-bump.txt.

Where the moved classes wentcore.Job, core.Step, core.JobExecutioncore.job.Job, core.step.Step, core.job.JobExecutioncore.JobParameters(Builder)core.job.parameters.*core.ChunkListenercore.listener.ChunkListeneritem.ItemReader, item.ItemWriter, item.support.*infrastructure.item.*
The picture lists the moves this small job needed. I confirmed each by listing the Batch 6.0.5 jars, not by reading release notes; the exact class names are in the table in the checklist further down. The errors are mechanical: a search and replace on import lines fixes all of them, which is what the scripted stage in the repository does. Compilation then passes, but with a wall of new warnings.

Step 2: it compiles, so what does the compiler say?

After the imports are ported, the compiler lists every API the job uses that Batch 6 has marked for removal. Below, the same build followed by the first start-up of the application:
$ mvn package   # imports ported to the new packages, dependencies unchanged (spring-boot-starter-batch)
[WARNING] s2/src/main/java/com/ankurm/batch/BatchConfig.java:[34,35] afterChunk(org.springframework.batch.core.scope.context.ChunkContext) in org.springframework.batch.core.listener.ChunkListener has been deprecated and marked for removal
[WARNING] s2/src/main/java/com/ankurm/batch/BatchConfig.java:[43,17] <I,O>chunk(int,org.springframework.transaction.PlatformTransactionManager) in org.springframework.batch.core.step.builder.StepBuilder has been deprecated and marked for removal
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[14,19] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[15,19] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[18,22] org.springframework.batch.core.launch.JobLauncher in org.springframework.batch.core.launch has been deprecated and marked for removal
[WARNING] s2/src/main/java/com/ankurm/batch/JobRunner.java:[18,44] org.springframework.batch.core.repository.explore.JobExplorer in org.springframework.batch.core.repository.explore has been deprecated and marked for removal
[WARNING] s2/src/test/java/com/ankurm/batch/JobTest.java:[16,16] org.springframework.batch.test.JobLauncherTestUtils in org.springframework.batch.test has been deprecated and marked for removal
[WARNING] s2/src/test/java/com/ankurm/batch/JobTest.java:[20,34] launchJob() in org.springframework.batch.test.JobLauncherTestUtils has been deprecated and marked for removal
[INFO] BUILD SUCCESS

$ java -jar target/batch-legacy-1.0.0.jar
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of method squareStep in com.ankurm.batch.BatchConfig required a bean of type 'org.springframework.transaction.PlatformTransactionManager' that could not be found.

Captured in 03-imports-ported-no-transaction-manager.txt.

There are two separate lessons here. The warnings are a to-do list: JobLauncher, JobExplorer, chunk(int, PlatformTransactionManager), the ChunkContext callback and the test utility are all “deprecated and marked for removal”. The start-up failure is not a warning: the application does not start.
Why there is no transaction manager now. With only spring-boot-starter-batch on the classpath, Boot 4 gives you a job repository that keeps nothing in a database, and it does not create a database transaction manager. My step asked for one and there was none. The starter that brings a JDBC-backed repository is a separate artifact, spring-boot-starter-batch-jdbc, plus spring-boot-starter-jdbc for the data source.
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.571 s -- in com.ankurm.batch.JobTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

$ java -jar target/batch-legacy-1.0.0.jar
WRITE [sq1, sq4, sq9]
CHUNK DONE, read so far=3
WRITE [sq16, sq25, sq36]
CHUNK DONE, read so far=6
WRITE [sq49]
CHUNK DONE, read so far=7
STATUS COMPLETED exit=COMPLETED
INSTANCES 1
DONE true

Captured in 04-jdbc-starter-added.txt.

With the JDBC starters added, the tests pass and the output is identical to the Batch 5 run. The transcript above is trimmed to the last lines; the full warning list is in the file linked below it. If you deliberately want the in-memory repository, define your own PlatformTransactionManager instead of adding the JDBC starters. I did not try that variant here.
Going deeper: the resourceless repository
The existing article on this site covers resourceless and JDBC-backed repositories in detail, including what a restart does in each. The short version is that a resourceless repository forgets everything on shutdown, so restartability is gone.

Step 3: the change that does not fail

Batch 6 adds a new chunk-oriented step. You get it by calling chunk(int) and setting the transaction manager separately, instead of chunk(int, transactionManager). The javap listing below shows both methods on StepBuilder and shows that the chunk listener interface now carries two families of callbacks: the old ones that receive a ChunkContext and new ones that receive the Chunk of items.
$ javap (Batch 6.0.5 jars), selected lines
public interface core.launch.JobOperator extends core.launch.JobLauncher {
public interface core.repository.JobRepository extends core.repository.explore.JobExplorer {
public class test.JobOperatorTestUtils extends test.JobLauncherTestUtils {
  public <I, O> core.step.builder.SimpleStepBuilder<I, O> chunk(int, PlatformTransactionManager);
  public <I, O> core.step.builder.ChunkOrientedStepBuilder<I, O> chunk(int);
  public <I, O> core.step.builder.SimpleStepBuilder<I, O> chunk(infrastructure.repeat.CompletionPolicy, PlatformTransactionManager);
  public default void beforeChunk(core.scope.context.ChunkContext);
  public default void afterChunk(core.scope.context.ChunkContext);
  public default void afterChunkError(core.scope.context.ChunkContext);
  public default void beforeChunk(infrastructure.item.Chunk<I>);
  public default void afterChunk(infrastructure.item.Chunk<O>);
  public default void onChunkError(java.lang.Exception, infrastructure.item.Chunk<O>);

Captured in 07-api-facts.txt.

Now make the one-line change from the old method to the new one and touch nothing else, including the listener:
[INFO] BUILD SUCCESS

$ java -jar target/batch-legacy-1.0.0.jar
WRITE [sq1, sq4, sq9]
WRITE [sq16, sq25, sq36]
WRITE [sq49]
STATUS COMPLETED exit=COMPLETED
INSTANCES 1
DONE true

Captured in 05-new-chunk-api-listener-silent.txt.

Look at the output. The three CHUNK DONE lines are gone. The job completes, the status is COMPLETED, the writes are right, and nothing complains. The listener still compiles because the old callback still exists on the interface. It is simply not called by the new step type. A monitoring or audit listener written this way would silently stop reporting.
The fix is to override the new callback, which receives the chunk of written items. It no longer has access to the running read count that the old callback read from the step context, so the message changes:
    @Bean
    ChunkListener<Integer, String> chunkListener() {
        return new ChunkListener<>() {
            @Override public void afterChunk(Chunk<String> chunk) {
                System.out.println("CHUNK DONE, items in chunk=" + chunk.size());
            }
        };
    }

    @Bean
    Step squareStep(JobRepository repo, PlatformTransactionManager tx, ChunkListener<Integer, String> listener) {
        return new StepBuilder("squareStep", repo)
                .<Integer, String>chunk(3)
                .transactionManager(tx)
                .reader(reader())
                .processor(i -> "sq" + (i * i))
                .writer(writer())
                .listener(listener)
                .build();
    }

Source: BatchConfig.java, lines 31–50.

$ mvn package   # final migrated project
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.029 s -- in com.ankurm.batch.JobTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

$ java -jar target/batch-migrated-1.0.0.jar
WRITE [sq1, sq4, sq9]
CHUNK DONE, items in chunk=3
WRITE [sq16, sq25, sq36]
CHUNK DONE, items in chunk=3
WRITE [sq49]
CHUNK DONE, items in chunk=1
STATUS COMPLETED exit=COMPLETED
INSTANCES 1
DONE true

Captured in 06-migrated.txt.

Which listener callback runs depends on which chunk() you callchunk(3, tx) (deprecated)calls afterChunk(ChunkContext)chunk(3).transactionManager(tx)calls afterChunk(Chunk)
The figure is a summary of the two runs above, not a statement about Batch internals: with the old method the transcript shows progress lines, with the new method and an unchanged listener it shows none. Search your code for every class that implements a Batch listener and check which callbacks it overrides before moving any step to the new method.

Step 4: replace the deprecated launch and query APIs

The listing above also shows how the launch API is arranged now: JobOperator extends JobLauncher, and JobRepository extends JobExplorer. So the migrated runner asks for the operator to start the job and the repository to count instances. The test utility gets the same treatment, JobOperatorTestUtils and startJob():
    private final JobOperator operator;
    private final JobRepository repository;
    private final Job job;

    public JobRunner(JobOperator operator, JobRepository repository, Job squareJob) {
        this.operator = operator; this.repository = repository; this.job = squareJob;
    }

    public void run() {
        try {
            JobParameters params = new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
            JobExecution exec = operator.start(job, params);
            System.out.println("STATUS " + exec.getStatus() + " exit=" + exec.getExitStatus().getExitCode());
            System.out.println("INSTANCES " + repository.getJobInstanceCount("squareJob"));
            System.out.println("DONE " + (exec.getStatus() == BatchStatus.COMPLETED));
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

Source: JobRunner.java, lines 14–32.


@SpringBootTest
@SpringBatchTest
class JobTest {
    @Autowired JobOperatorTestUtils utils;

    @Test
    void jobCompletes() throws Exception {
        JobExecution exec = utils.startJob();
        assertThat(exec.getStatus()).isEqualTo(BatchStatus.COMPLETED);
    }
}

Source: JobTest.java, lines 12–24.

With those changes the final build has no deprecation warnings, as the first lines of the migrated transcript above show.
Going deeper: what I did not need to change
The job builders keep the same constructor shape: new JobBuilder(name, repository) and new StepBuilder(name, repository). I did not use the surviving pieces of Batch 5 that are more elaborate than this job: partitioning, remote chunking, flows, or fault-tolerant steps. Those may have their own moves; the partitioning article on this site is a good test case for the same steps.

The checklist

Batch 5Batch 6.0.5How I know
core.Job, core.Step, core.JobExecutioncore.job.Job, core.step.Step, core.job.JobExecutioncompile errors, then a passing build
core.JobParameters, JobParametersBuildercore.job.parameters.*same
core.ChunkListenercore.listener.ChunkListener (now generic)same
batch.item.*batch.infrastructure.item.*same
core.explore.JobExplorercore.repository.explore.JobExplorer, deprecated; use JobRepositorywarnings, javap
JobLauncherdeprecated; use JobOperator.start(job, params)warnings, javap
chunk(n, tx)deprecated; use chunk(n).transactionManager(tx), and re-check listenerswarnings, silent run
JobLauncherTestUtils.launchJob()JobOperatorTestUtils.startJob()warnings, passing test
spring-boot-starter-batch gives a DBadd spring-boot-starter-batch-jdbc and spring-boot-starter-jdbcstart-up failure, then success

Should you migrate now?

Yes, if you are already moving to Boot 4. Batch 6 comes with Boot 4, so there is no separate decision. Do it in the order above: fix imports, run the app, then remove deprecations one at a time and diff the behaviour of every listener. Plan a manual check for anything that writes an audit or progress trail. I tested one job with one step; a real project with partitioned or remote steps needs its own run, and Batch 6.1 is still in milestones.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.