Boost Spring Batch Performance: Async & Virtual Threads in Spring Boot 3

This article demonstrates how to accelerate Spring Batch jobs in Spring Boot 3 by comparing a default synchronous job with asynchronous processing using ThreadPoolTaskExecutor and then with virtual threads, providing code examples, configuration steps, and performance results that show up to a 90‑fold speedup.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Boost Spring Batch Performance: Async & Virtual Threads in Spring Boot 3

Environment

Spring Boot 3.4.2

1. Introduction

Spring Batch is a batch‑processing framework designed for large data sets. It offers high scalability, robust error handling, and transaction management, making it suitable for tasks such as periodic report generation and massive data migration.

2. Practical Cases

2.1 Default Synchronous Job

Define ItemReader, ItemProcessor, and ItemWriter components and configure a synchronous job. The job runs automatically on startup and takes about 54 seconds for the sample data set.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch</artifactId>
</dependency>
spring:
  batch:
    jdbc:
      initialize-schema: always
@Component
public class NumberItemReader implements ItemReader<Integer> {
  private static final Integer UPPER_BOUND = 500;
  private int currentIndex = 0;
  public Integer read() {
    return (currentIndex < UPPER_BOUND) ? currentIndex++ : null;
  }
}
@Component
public class SyncItemProcessor implements ItemProcessor<Integer, Integer> {
  private static final Logger LOGGER = LoggerFactory.getLogger(SyncItemProcessor.class);
  @Override
  public Integer process(@NonNull Integer item) throws Exception {
    LOGGER.info("Processing item {}", item);
    TimeUnit.MILLISECONDS.sleep(100);
    return item;
  }
}
@Component
public class LogItemWriter implements ItemWriter<Integer> {
  private static final Logger LOGGER = LoggerFactory.getLogger(LogItemWriter.class);
  @Override
  public void write(Chunk<? extends Integer> chunk) {
    for (var item : chunk) {
      LOGGER.info("Writing item {}", item);
    }
  }
}
@Configuration
public class BatchConfiguration {
  public static final String SYNC_JOB_NAME = "syncJob";
  @Bean
  Step syncStep(JobRepository jobRepository, PlatformTransactionManager transactionManager,
                NumberItemReader numberItemReader, SyncItemProcessor syncItemProcessor,
                LogItemWriter logItemWriter) {
    return new StepBuilder("sync-step", jobRepository)
        .<Integer, Integer>chunk(100, transactionManager)
        .reader(numberItemReader)
        .processor(syncItemProcessor)
        .writer(logItemWriter)
        .build();
  }
  @Bean(SYNC_JOB_NAME)
  Job syncJob(JobRepository jobRepository, Step syncStep) {
    return new JobBuilder(SYNC_JOB_NAME, jobRepository)
        .incrementer(new RunIdIncrementer())
        .start(syncStep)
        .build();
  }
}

Result: total execution time ≈ 54 s.

2.2 Asynchronous Job Configuration

Add the spring-batch-integration dependency and configure an AsyncItemProcessor and AsyncItemWriter backed by a ThreadPoolTaskExecutor with 5‑10 threads.

<dependency>
  <groupId>org.springframework.batch</groupId>
  <artifactId>spring-batch-integration</artifactId>
</dependency>
@Configuration
public class AsyncBatchConfiguration {
  public static final String STEP_NAME = "async-step";
  public static final String ASYNC_JOB_NAME = "asyncJob";

  @Bean
  AsyncItemProcessor<Integer, Integer> asyncItemProcessor(SyncItemProcessor delegate,
                                                          TaskExecutor executor) {
    var processor = new AsyncItemProcessor<Integer, Integer>();
    processor.setDelegate(delegate);
    processor.setTaskExecutor(executor);
    return processor;
  }

  @Bean
  AsyncItemWriter<Integer> asyncWriter(LogItemWriter delegate) {
    var writer = new AsyncItemWriter<Integer>();
    writer.setDelegate(delegate);
    return writer;
  }

  @Bean
  TaskExecutor threadPoolTaskExecutor() {
    var executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(10);
    executor.setThreadNamePrefix("platform-thread-");
    return executor;
  }

  @Bean
  Step asyncStep(JobRepository jobRepository, PlatformTransactionManager transactionManager,
                NumberItemReader numberItemReader, AsyncItemProcessor<Integer, Integer> asyncProcessor,
                AsyncItemWriter<Integer> asyncWriter) {
    return new StepBuilder(STEP_NAME, jobRepository)
        .<Integer, Future<Integer>>chunk(100, transactionManager)
        .reader(numberItemReader)
        .processor(asyncProcessor)
        .writer(asyncWriter)
        .build();
  }

  @Bean(ASYNC_JOB_NAME)
  Job asyncJob(JobRepository jobRepository, Step asyncStep) {
    return new JobBuilder(ASYNC_JOB_NAME, jobRepository)
        .incrementer(new RunIdIncrementer())
        .start(asyncStep)
        .build();
  }
}

Result: total execution time ≈ 11 s (about 5× faster than the synchronous version).

2.3 Virtual Thread Processing

Replace the thread‑pool executor with a VirtualThreadTaskExecutor to leverage Java virtual threads.

@Bean
TaskExecutor threadPoolTaskExecutor() {
  VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("virtual-thread-");
  return executor;
}

Result: total execution time ≈ 0.6 s, an 18× improvement over the async thread‑pool version and roughly 90× faster than the original synchronous job.

Sync job execution time
Sync job execution time
Async job execution time
Async job execution time
Virtual thread job execution time
Virtual thread job execution time
Virtual ThreadsSpring Batchasync-processingSpring Boot 3
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.