skip to content

Scaling & Parallel Processing

Making a batch job faster: multiple threads in one step, partitioning work across workers, remote chunking, and parallel steps. Interviewers ask which one fits a given bottleneck, which requires knowing whether you are read-bound or write-bound.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

20

What is a multithreaded step in Spring Batch, and how do you enable it?

level: juniorimportance: must knowfreq 55%

answer

  1. taskExecutor(...) on the step builder
  2. one shared reader across threads
  3. SynchronizedItemStreamReader for stateful readers
  4. throttleLimit = chunks in flight (deprecated in 5)
  5. no ordering / weak restart

basics

~20 s

A multithreaded step processes chunks on several threads inside one JVM instead of one. You enable it by giving the step a TaskExecutor (e.g. step.taskExecutor(...)), so each chunk runs on a separate thread from the pool.

solid answer

~40 s

A multithreaded step is Spring Batch's simplest scaling technique: within a single step and single JVM, chunks are dispatched to a thread pool instead of being processed sequentially on one thread. You configure it in the StepBuilder with .taskExecutor(taskExecutor), passing a Spring TaskExecutor such as a ThreadPoolTaskExecutor (never SyncTaskExecutor, which stays single-threaded). Each worker thread reads a chunk, processes and writes it concurrently with other threads. Because the same ItemReader instance is shared across threads, the big caveat is that the reader (and any stateful component) must be thread-safe — most built-in readers are not, so you wrap them in SynchronizedItemStreamReader. throttleLimit historically capped the number of concurrent chunks. It boosts throughput on I/O-bound steps without the setup cost of partitioning or remote workers.

code

java · 22 lines
java
@Bean
public Step multithreadedStep(JobRepository jobRepository,
                               PlatformTransactionManager tx,
                               ItemReader<String> delegate,
                               ItemWriter<String> writer) {
    // Wrap the stateful reader so read() is synchronized
    SynchronizedItemStreamReader<String> safeReader = new SynchronizedItemStreamReader<>();
    // NOTE: delegate must be an ItemStreamReader for this to compile
    // safeReader.setDelegate((ItemStreamReader<String>) delegate);

    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(8);
    executor.setMaxPoolSize(8);
    executor.afterPropertiesSet();

    return new StepBuilder("multithreadedStep", jobRepository)
            .<String, String>chunk(100, tx)
            .reader(safeReader)
            .writer(writer)
            .taskExecutor(executor)   // dispatch chunks across the pool
            .build();
}

go deeper

for a junior

Know it's the simplest scaling option: add a TaskExecutor to the step so chunks run on multiple threads in one JVM.

for a middle

Explain the shared-reader hazard and SynchronizedItemStreamReader; know ThreadPoolTaskExecutor vs SyncTaskExecutor.

for a senior

Discuss throttleLimit (deprecated in 5), ordering loss, weakened restartability, and I/O-bound vs CPU-bound suitability.

for a principal

Weigh multithreaded step against partitioning/remote chunking; reason about restart semantics, idempotency, and when the shared-state risk outweighs the simplicity.

## The problem it solves By default a Spring Batch **chunk-oriented step** runs on a single thread: it reads N items (the chunk size), processes them, writes them in one transaction, commits, then repeats — strictly sequentially. If each write is I/O-bound (a DB insert, an HTTP call), the single thread spends most of its time waiting, leaving CPU idle. A **multithreaded step** lets multiple chunks be processed **concurrently on several threads inside the same JVM**. ## How to enable it You attach a Spring `org.springframework.core.task.TaskExecutor` to the step via the builder: ```java new StepBuilder("myStep", jobRepository) .<In, Out>chunk(100, transactionManager) .reader(reader).processor(processor).writer(writer) .taskExecutor(taskExecutor) // <-- makes it multithreaded .build(); ``` Internally the step's `RepeatTemplate` (a `TaskExecutorRepeatTemplate`) submits each chunk iteration to the executor. Use a real pooled executor like `ThreadPoolTaskExecutor`. If you pass `SyncTaskExecutor` (the default) it runs on the caller thread and stays single-threaded. ## The core gotcha: shared, stateful components There is exactly **one** `ItemReader` instance for the step, shared by every worker thread. Most built-in readers are **stateful** — e.g. `JdbcCursorItemReader` walks a JDBC cursor, `FlatFileItemReader` tracks the current line, `JpaPagingItemReader` tracks a page counter. Calling `read()` from multiple threads without synchronization corrupts that state: skipped rows, duplicates, `ArrayIndexOutOfBoundsException`, etc. The fix is `org.springframework.batch.item.support.SynchronizedItemStreamReader`, which wraps your reader and makes `read()` `synchronized`: ```java SynchronizedItemStreamReader<T> safe = new SynchronizedItemStreamReader<>(); safe.setDelegate(realReader); ``` (There is a matching `SynchronizedItemStreamWriter` if your writer is not thread-safe.) Note synchronizing `read()` serializes the reading — the parallelism win comes from processing and writing running concurrently, not reading. ## throttleLimit `throttleLimit` bounds how many chunks may be **in flight** (submitted but not yet completed) at once, independent of the executor's own pool size. Classically it defaulted to 4. In Spring Batch 5 the `throttleLimit(int)` builder method is **deprecated**; the recommended control is to size the `TaskExecutor`'s pool (core/max pool size, queue capacity) directly. Conceptually it prevents the step from flooding the executor's queue faster than workers can drain it. ## Ordering and restart caveats - **No ordering guarantee.** Chunks are processed concurrently, so items are read, written and committed out of order. Don't use it if downstream logic depends on input order. - **Restartability is weakened.** Spring Batch tracks progress by a single read-count in `BATCH_STEP_EXECUTION`. With concurrency, if the job fails mid-flight there can be gaps (chunk 5 committed but chunk 4 didn't), so re-running may reprocess or skip. For this reason multithreaded steps are best for **restart-from-scratch** / idempotent workloads, and `saveState(false)` is sometimes set on readers. - **Not fault-tolerant-friendly.** Combining with skip/retry and stateful readers multiplies the concurrency hazards. ## When to use vs alternatives Use a multithreaded step for a quick throughput win on **one JVM**, **I/O-bound** steps, where strict ordering and exact restart aren't required. When you need bounded data partitions, per-partition restartability, or to scale across JVMs, prefer **local/remote partitioning** (`Partitioner` + `PartitionHandler`) or **remote chunking**, which give each worker its own reader instance and avoid the shared-state problem entirely.

  • Why can't you just pass any TaskExecutor and expect a speedup?
    SyncTaskExecutor (the default) runs the chunk on the caller's thread, so it stays single-threaded. You need a truly pooled executor like ThreadPoolTaskExecutor, and the step must actually be I/O-bound to benefit.
  • What breaks if the reader isn't made thread-safe?
    Multiple threads call read() on one stateful reader concurrently, corrupting its internal cursor/line/page counter — causing duplicate items, skipped items, or exceptions. Wrap it in SynchronizedItemStreamReader.

saying these in an interview costs you the question

  • Thinking a multithreaded step spins up multiple JVMs (it's one JVM, one process)
  • Assuming each thread gets its own reader instance (there's one shared reader)
  • Believing SyncTaskExecutor gives parallelism
  • Expecting items to stay in input order

context

open as a page

What is partitioning in Spring Batch, and why would you use it?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Partitioning splits a step's work into several named partitions that run in parallel — each processes a slice of the data (e.g. an ID range). It scales a long-running step by using multiple threads or machines.

open as a page

What is Remote Chunking in Spring Batch, and which part of the chunk-oriented step runs where?

level: juniorimportance: must knowfreq 45%

basics

~20 s

A scaling pattern where one master step reads items and sends chunks over messaging (like a queue) to remote worker processes. The workers do the processing and writing, then reply. Reading stays central; processing/writing scales out.

open as a page

How do you configure a split with FlowBuilder and a TaskExecutor, and what does each call do?

level: middleimportance: must knowfreq 50%

basics

~10 s

Build each branch as a Flow, then call new FlowBuilder(name).split(taskExecutor).add(flowA, flowB).build(). split() supplies the thread pool, add() registers the concurrent branches, and the resulting flow becomes a step in the job.

open as a page

How does the Partitioner interface work, and how does a worker step read its assigned slice?

level: middleimportance: must knowfreq 62%

basics

~10 s

Partitioner has one method: partition(int gridSize) returning Map<String, ExecutionContext>. Each entry is a named partition whose ExecutionContext holds that slice's parameters (e.g. minId/maxId). The worker's @StepScope reader reads them via SpEL like #{stepExecutionContext['minId']}.

open as a page

What are the roles of ChunkMessageChannelItemWriter and ChunkProcessorChunkHandler in a remote chunking setup?

level: middleimportance: must knowfreq 35%

basics

~20 s

ChunkMessageChannelItemWriter is the master's ItemWriter: instead of writing, it sends chunks to the request channel and collects worker replies. ChunkProcessorChunkHandler is the worker's handler: it receives a chunk, runs the processor+writer, and sends back a response.

open as a page

Why are most Spring Batch ItemReaders unsafe in a multithreaded step, and how do you fix it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

There is one reader instance shared by all threads, and most readers hold mutable state (a cursor, current line, or page index). Concurrent read() calls corrupt that state. Wrap the reader in SynchronizedItemStreamReader so read() is synchronized.

open as a page

How does Remote Chunking differ from Remote Partitioning, and how do you choose between them?

level: seniorimportance: must knowfreq 55%

basics

~20 s

In remote chunking the master reads all data and ships the actual items to workers, which process+write. In remote partitioning the master only computes partition ranges (metadata); each worker reads, processes, and writes its own slice. Choose chunking when the read is cheap but process/write is heavy; partitioning when the read scales too.

open as a page

What is a parallel step (split) in Spring Batch, and what problem does it solve?

level: juniorimportance: should knowfreq 45%

basics

~10 s

A split runs several independent steps/flows at the same time within one job instead of one after another, using a thread pool. The job waits for all of them to finish before moving on.

open as a page

What does throttleLimit control in a multithreaded step, and how has its role changed in Spring Batch 5?

level: middleimportance: should knowfreq 35%

basics

~20 s

throttleLimit caps how many chunks can be processed concurrently (in flight at once), regardless of pool size. In Spring Batch 5 the throttleLimit(...) builder method is deprecated; you instead size the TaskExecutor's thread pool directly.

open as a page

What thread-safety and executor concerns arise from running steps in a split, and how do you mitigate them?

level: middleimportance: should knowfreq 30%

basics

~10 s

Branches run on different threads, so any shared mutable state can race. Use a bounded ThreadPoolTaskExecutor (not unbounded SimpleAsyncTaskExecutor), keep readers/writers per-branch or thread-safe, and size the pool against your DB connection pool.

open as a page

What are the restartability, ordering, and fault-tolerance limitations of a multithreaded step, and when should you avoid it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Chunks run concurrently, so items are processed out of order and commit out of order. On failure the single read-count can't cleanly mark a resume point, so restart may skip or reprocess items. Avoid it when order matters, restart must resume exactly, or you rely heavily on skip/retry.

open as a page

In a split, what happens when one branch fails, and how does the join/status aggregation work? Is the job restartable?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The split waits for every branch to finish (join), then aggregates statuses: if any branch failed, the whole split is FAILED, so the job fails. On restart, completed branches/steps are skipped and only the failed ones re-run.

open as a page

What is the role of PartitionHandler and TaskExecutorPartitionHandler in partitioning?

level: seniorimportance: should knowfreq 50%

basics

~10 s

The PartitionHandler executes the worker StepExecutions produced from the partitions and collects their results. TaskExecutorPartitionHandler is the local implementation: it runs each worker on a TaskExecutor (thread pool) and waits for all to finish.

open as a page

How do restart, StepExecution metadata, and thread-safety work for a partitioned step?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Each partition is a separate worker StepExecution with its own metadata and transactions. On restart, completed partitions are skipped and only failed/unfinished ones rerun. Because partitions run concurrently, reader/writer beans must be @StepScope or otherwise thread-safe.

open as a page

Why must the messaging middleware in Remote Chunking provide guaranteed delivery, and what goes wrong otherwise?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Because the actual items travel in the messages. If a request or response message is lost, those items are never processed/written or the master never learns a chunk finished — causing silent data loss or a hung/failed step. So you need a durable broker with acknowledgements.

open as a page

Compare a multithreaded step with partitioning and remote chunking. When would you choose each?

level: principalimportance: should knowfreq 30%

basics

~20 s

Multithreaded step: many threads share one reader in one JVM — simplest, but shared state and weak restart. Partitioning: each worker step gets its own reader over a data slice — clean restart, scales locally or across JVMs. Remote chunking: one master reads, workers process/write remotely — for CPU/write-heavy work.

open as a page

As an architect, how do you choose between a split, a multi-threaded step, and partitioning for scaling a batch job?

level: principalimportance: should knowfreq 40%

basics

~20 s

Use a split to run different, independent steps in parallel. Use a multi-threaded step to run chunks of one step across threads. Use partitioning to run the same step over many data slices, optionally across machines. Split parallelizes work; partitioning parallelizes data.

open as a page

When would you choose partitioning over a multi-threaded step, and how do you decide the partitioning strategy?

level: principalimportance: should knowfreq 34%

basics

~20 s

Choose partitioning when work splits cleanly into independent slices and you want per-slice restart or to scale across machines. A multi-threaded step parallelizes one StepExecution's chunks but needs a thread-safe reader and gives up clean restart. Partition on a stable, evenly-distributed key.

open as a page

How does the remote-chunking master track completion and fault tolerance across asynchronous workers, and what are the operational limits?

level: principalimportance: should knowfreq 22%

basics

~20 s

The master's ChunkMessageChannelItemWriter counts chunks sent versus ChunkResponses received; the step only completes when all outstanding responses arrive. A failure flag in a response fails the step. The main limits: the single reader, throttle/timeout on outstanding chunks, and worker-side skip/retry not being visible to the master.

open as a page