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.
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 onlyspring-boot-starter-batchon 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, plusspring-boot-starter-jdbcfor 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 ownPlatformTransactionManager 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.
- Companion for that article: spring-boot-demo/spring-batch
- Spring Batch on Boot 4.1
Step 3: the change that does not fail
Batch 6 adds a new chunk-oriented step. You get it by callingchunk(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 threeThe 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:CHUNK DONElines are gone. The job completes, the status isCOMPLETED, 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.
@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.
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 5 | Batch 6.0.5 | How I know |
|---|---|---|
core.Job, core.Step, core.JobExecution | core.job.Job, core.step.Step, core.job.JobExecution | compile errors, then a passing build |
core.JobParameters, JobParametersBuilder | core.job.parameters.* | same |
core.ChunkListener | core.listener.ChunkListener (now generic) | same |
batch.item.* | batch.infrastructure.item.* | same |
core.explore.JobExplorer | core.repository.explore.JobExplorer, deprecated; use JobRepository | warnings, javap |
JobLauncher | deprecated; use JobOperator.start(job, params) | warnings, javap |
chunk(n, tx) | deprecated; use chunk(n).transactionManager(tx), and re-check listeners | warnings, silent run |
JobLauncherTestUtils.launchJob() | JobOperatorTestUtils.startJob() | warnings, passing test |
spring-boot-starter-batch gives a DB | add spring-boot-starter-batch-jdbc and spring-boot-starter-jdbc | start-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.
No Comments yet!