Architecture & processing strategies
|
This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
Spring Batch is layered so that business logic, batch runtime and reusable plumbing stay separate, and it leaves the processing strategy — how a workload is split up in time and across processes — as an explicit design decision. This page covers both.
The three layers
your Job/Step configuration, ItemProcessors, Tasklets, business services"] Core["Batch core
Job, Step, JobRepository, JobOperator, builders, flows, listeners, partitioning"] Infra["Batch infrastructure
ItemReaders, ItemWriters, RepeatTemplate, RetryTemplate, ItemStream"] App --> Core App --> Infra Core --> Infra
Application is the code written for a particular business problem: the job and step definitions, the processors, any tasklets, and the services they call. It is the only layer that changes per project.
Batch core (spring-batch-core) is the runtime that launches and controls a job: the domain classes, the
JobRepository that records executions, the JobOperator that starts and stops them, the builders, flow
handling, listeners and partitioning.
Batch infrastructure (spring-batch-infrastructure) holds the reusable, problem-agnostic pieces — the
readers and writers, RepeatOperations, retry support, and the ItemStream contract. Both the application
and the core layer build on it, and it can be used without the core layer at all.
The stereotypes that populate these layers — Job, Step, JobRepository, JobOperator,
ItemReader/ItemProcessor/ItemWriter — are introduced in
Jobs, instances & parameters and
Steps, executions & the ExecutionContext. The
layering itself is described in
Batch processing and Spring
Batch’s architecture.
A minimal job touches all three layers without naming them:
@Bean
public Step loadCustomers(JobRepository jobRepository, PlatformTransactionManager tx,
ItemReader<CustomerCsv> reader, // infrastructure
ItemProcessor<CustomerCsv, Customer> processor, // application
ItemWriter<Customer> writer) { // infrastructure
return new StepBuilder("loadCustomers", jobRepository) // core
.<CustomerCsv, Customer>chunk(500, tx)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
General batch principles and guidelines
These are the design rules that make a batch program survivable in production:
-
Make steps idempotent. A step that is rerun after a crash must not double-count. Prefer writes that can be replayed — upserts keyed on a natural key, or a watermark that advances only on commit.
-
Keep each step small and single-purpose. One step should do one thing: stage a file, load it, aggregate it, archive it. Small steps restart cheaply and fail informatively.
-
Simplify aggressively. Batch code is read at 3 a.m. Complex conditional flows are harder to reason about than three sequential steps.
-
Minimise system I/O. Read and write in chunks, not per item. Avoid re-reading the same data in several steps; stage it once.
-
Do not do in a batch what a database does better. Set-based updates, sorting and aggregation are usually cheaper in SQL than row-by-row in Java — see Tasklet steps for wrapping such a statement in a step.
-
Log liberally, and count. Every
StepExecutionalready records read/write/skip counts; add business-level logging at chunk boundaries, not per item. -
Allocate enough memory to avoid re-reads, but no more. Large in-memory caches defeat chunking.
-
Validate input at the boundary and assume nothing downstream — see ItemProcessors.
-
Plan and execute stress tests early with production-scale volumes; batch problems are volume problems.
-
Build checks into the job, not into a runbook: row counts, control totals and reconciliation as steps that fail the job.
Processing strategies
The normal batch window
The simplest arrangement: online access is closed (or quiescent), and the batch owns the data for a fixed window. There is no contention, so no locking strategy is needed and jobs can use the fastest bulk techniques. The design constraint is purely the clock — the work must fit the window, which is what makes the scaling options in Scaling & parallel processing relevant.
Concurrent batch and online processing
When the online system stays available, batch and online transactions contend for the same rows. Options, in increasing order of intrusiveness:
-
keep batch transactions short (a small commit interval) so locks are held briefly;
-
use optimistic locking — a version column checked on update, so a conflicting online change fails the item rather than blocking it;
-
use pessimistic locking on a narrow set of rows for the duration of one chunk;
-
use a logical lock: a dedicated lock-flag column or lock table that both the online application and the batch respect, so the batch marks a record as "being processed" and the online path refuses or defers.
The transaction-isolation trade-offs behind these are covered in SQL Transactions and Transaction Isolation & Locking.
Parallel processing
Independent jobs, or independent steps within a job, run at the same time. This is the cheapest speed-up when the workloads do not touch the same data: split flows inside one job (Step flow & listeners) or simply schedule separate jobs concurrently. The prerequisite is that the parallel units share no mutable state and no contended table.
Partitioning
One logically identical step is run many times over disjoint slices of the input. Partitioning is the
strategy that scales a single large step, and it is described in detail — with the Partitioner,
PartitionHandler and worker-step machinery — in
Scaling & parallel processing.
The strategic question is how to break the input up. The classic approaches:
| Approach | How the input is split |
|---|---|
Fixed-length (record count) |
Each partition gets a fixed number of records — partition 1 takes records 1..10 000, partition 2 takes 10 001..20 000. Simple; assumes uniform per-record cost. |
Range (key range) |
Each partition owns a contiguous key range — customer IDs |
List (explicit breakdown) |
A separate control table or file lists the partitions explicitly — one per branch, region, or file. Gives full control and allows an operator to add or remove partitions without code changes. |
Hashing |
A hash of the key modulo the grid size decides the partition. Distributes evenly regardless of key skew, but destroys locality, so each partition’s reads scatter across the table. |
Modulus |
A numeric key modulo the grid size ( |
Custom |
Any project-specific rule — one partition per input file, per tenant, per upstream feed — implemented by
writing a |
A Partitioner returning a per-partition ExecutionContext is the code form of whichever approach is chosen:
public class ModulusPartitioner implements Partitioner {
@Override
public Map<String, ExecutionContext> partition(int gridSize) {
Map<String, ExecutionContext> partitions = new HashMap<>();
for (int i = 0; i < gridSize; i++) {
ExecutionContext context = new ExecutionContext();
context.putInt("remainder", i);
context.putInt("divisor", gridSize);
partitions.put("partition" + i, context);
}
return partitions;
}
}
Whichever strategy is chosen, decide it before writing the step: partitioned steps need a restartable, stateless reader and a writer that tolerates concurrency, which constrains the choices made in ItemReaders: databases.