Spring Batch — MultiThreaded Step (Parallel Processing Deep Dive)

Spring Batch — MultiThreaded Step (Parallel Processing Deep Dive)

Processing millions of records sequentially on a single thread is a massive bottleneck in enterprise applications. To achieve high throughput, Spring Batch allows you to run a single step across multiple threads concurrently using a TaskExecutor.

However, simply adding a thread pool to your batch job is dangerous. If you do not understand thread-safety, transaction boundaries, and connection pool sizing, you will encounter data corruption, deadlocks, and database connection timeouts.

๐Ÿ“บ Visual Learner? Master Spring Batch on YouTube!
Watch full implementations of multi-threaded steps, partitioning, and performance tuning in my complete video series.

▶ Watch the Spring Batch Masterclass Playlist

1. Multi-threaded Step vs Partitioning

Before implementing a multi-threaded step, ensure it is the right architectural choice for your workload.

AspectMulti-threaded StepPartitioning
Concurrency ModelMultiple threads execute the exact same Step instance.A Master step divides data into subsets; worker steps process subsets independently.
Use CaseMedium datasets where reading/writing is fast, but processing is heavy.Massive datasets that can be easily split by ID ranges or dates.
State ManagementHigh Risk: Threads share memory. Readers must be strictly synchronized.Low Risk: Each partition gets its own isolated Reader and Writer instances.

2. The Number One Gotcha: Thread-Safe Readers

๐Ÿšจ Critical Danger: Standard Spring Batch readers (like FlatFileItemReader or JdbcCursorItemReader) are stateful. They keep track of the current line/cursor. If multiple threads access them simultaneously, you will read duplicate data or skip records entirely!

To safely use a multi-threaded step, you must wrap your reader in a SynchronizedItemStreamReader. This ensures that only one thread can pull an item from the file/database at a time, while the heavy processing and writing happen in parallel.

import org.springframework.batch.item.support.builder.SynchronizedItemStreamReaderBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;

@Bean
public SynchronizedItemStreamReader<Employee> threadSafeReader(FlatFileItemReader<Employee> delegateReader) {
    return new SynchronizedItemStreamReaderBuilder<Employee>()
            .delegate(delegateReader)
            .build();
}

3. Configuring the Multi-threaded Step (Spring Batch 5)

Here is how to configure the Step using the modern Spring Batch 5 StepBuilder. We inject our thread-safe reader and a custom ThreadPoolTaskExecutor.

import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class MultiThreadedStepConfig {

    @Bean
    public Step parallelStep(JobRepository jobRepository, 
                             PlatformTransactionManager txManager,
                             TaskExecutor batchTaskExecutor) {
                             
        return new StepBuilder("parallelStep", jobRepository)
                .<Input, Output>chunk(100, txManager)
                .reader(threadSafeReader())
                .processor(statelessProcessor())
                .writer(threadSafeWriter())
                .taskExecutor(batchTaskExecutor)
                // Prevents runaway thread creation
                .throttleLimit(15) 
                .build();
    }

    @Bean
    public TaskExecutor batchTaskExecutor() {
        ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
        exec.setCorePoolSize(10);
        exec.setMaxPoolSize(20);
        exec.setQueueCapacity(50);
        exec.setThreadNamePrefix("batch-thread-");
        exec.initialize();
        return exec;
    }
}

4. Deep Dive: Tuning the ThreadPoolTaskExecutor

A poorly tuned thread pool is worse than single-threaded execution. Here is exactly how Spring handles the variables in the code above:

๐Ÿ” Core Pool Size: setCorePoolSize(10)

When the step starts, Spring immediately utilizes up to 10 threads. These threads stay alive during the batch job, reducing startup latency.

๐Ÿ” Queue Capacity: setQueueCapacity(50)

If all 10 core threads are busy processing chunks, new incoming tasks are placed in this queue. A large queue smooths out CPU spikes but delays thread creation.

๐Ÿ” Max Pool Size: setMaxPoolSize(20)

Important: Max pool size is only reached if the core threads are full AND the queue is completely full (50 items). If the queue fills up and 20 threads are maxed out, you will receive a TaskRejectedException.

Order of Execution for Capacity Expansion:
1) Use up to corePoolSize threads.
2) If busy → place chunks in the queue.
3) If queue is full → create extra threads up to maxPoolSize.
4) If max threads reached & queue is full → Reject Task.

5. Crucial Database Considerations

When you introduce multi-threading, you are introducing concurrent database connections. Each thread handling a chunk will open its own database transaction.

  • HikariCP Tuning: Your database connection pool (e.g., HikariCP) maximum size must be larger than your maxPoolSize. If you have 20 batch threads but only 10 DB connections, your job will instantly deadlock waiting for connections. Set spring.datasource.hikari.maximum-pool-size=25.
  • Database Locks: If multiple threads try to update the exact same database row simultaneously, you will hit an OptimisticLockingFailureException or a deadlock. Ensure your chunk data does not contain duplicate entities.

✅ Best Practices Checklist

  • Always wrap stateful readers with SynchronizedItemStreamReaderBuilder.
  • Ensure your ItemProcessor is completely stateless (do not use class-level variables).
  • Set your DB connection pool size slightly higher than your thread pool's maxPoolSize.
  • Keep chunk sizes relatively small (50-200) to ensure threads complete work quickly and don't hold DB transactions open too long.

๐Ÿงต Related Spring Batch Performance Guides

Enhance your understanding of parallel batch processing by exploring how multithreaded steps interact with error handling and job flow control.

๐Ÿงฑ Spring Batch Core Components

Understand how Job, Step, and Chunk Contexts behave when executed across multiple parallel threads.

๐Ÿ”„ Spring Batch ItemProcessor Example

Learn how to design strictly stateless ItemProcessor logic to prevent data corruption during parallel batch processing.

๐Ÿ“ฅ CSV to Database with Spring Batch

Speed up massive CSV imports by combining multithreaded steps with high-performance JdbcBatchItemWriters.

๐Ÿšซ Skip Policy & Error Handling

Understand how skip limitations and fault-tolerant settings behave when exceptions are thrown concurrently.

๐Ÿ”€ Conditional Flow in Spring Batch Jobs

Control execution paths and trigger split parallel flows based on the success or failure of your multithreaded steps.

⚙️ Spring Batch Tasklet

Learn when a simple Tasklet is a safer, more effective alternative to complex multithreaded chunk processing.