Java, Programming
Spring Batch: A Practical Guide to Restartable Jobs
· Eric B.
Handling thousands or millions of records is a common enterprise problem. Spring Batch turns that work into jobs and steps with built-in transaction management, restart, skip, retry, and execution statistics. This guide moves from components and core concepts to a working application, then covers advanced behavior, testing, monitoring, and scaling, with every example written for Spring Batch 6.
Decide whether the workload is actually a batch job
Spring Batch fits a bounded dataset, repeatable business process, and clear completion state. Imports, exports, billing runs, report generation, reconciliation, and nightly transformations all have that shape.
Spring Batch differs from three adjacent tools:
- A scheduler decides when work starts. Quartz, Kubernetes CronJob, Control-M, or a cloud scheduler can launch a Spring Batch job.
- A message consumer processes an open-ended stream of events and normally has no final completed state.
- A web request expects a short response and is a poor place to hold an HTTP thread while millions of rows process.
The Spring Batch introduction explicitly describes the framework as a batch engine that works with schedulers rather than replacing one.
Understand the 8 core Spring Batch entities
Eight components explain most of a batch application’s lifecycle: Job, JobInstance, JobExecution, Step, StepExecution, JobRepository, ExecutionContext, and JobOperator.
| Entity | Responsibility |
|---|---|
Job | Declares the ordered flow of steps |
JobInstance | Identifies a logical run from the job name and identifying parameters |
JobExecution | Records one attempt to run a job instance |
Step | Defines one phase of work |
StepExecution | Records one attempt to execute a step |
JobRepository | Persists job and step metadata |
ExecutionContext | Stores restart state for a job or step |
JobOperator | Starts, stops, restarts, and inspects jobs operationally |
One JobInstance can have several JobExecution attempts after failures. Launching the same job with the same identifying parameters does not create a new logical instance. Parameters such as business date and input filename therefore need deliberate identity rules.
Choose chunk processing or a tasklet
Chunk processing fits repeated items, while a tasklet fits one bounded action. A chunk step calls an ItemReader, optional ItemProcessor, and ItemWriter, then commits after a configured number of items.
If the commit interval is 100, the framework reads and processes up to 100 items, writes them, commits the transaction, and updates step metadata. A failure rolls back the active transaction, not every chunk that completed earlier.
A tasklet fits operations such as deleting a temporary file, calling a stored procedure once, or validating an input directory. Hiding a record loop inside a tasklet discards much of the framework’s item-level restart and fault-tolerance support.
Create the Spring Boot project and database
Start the application with the Spring Batch starter, JDBC support, a production database driver, and the batch test module. This article targets the Spring Batch 6 reference line, whose published documentation currently identifies version 6.0.5.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Use a persistent JDBC or MongoDB JobRepository when the job needs restartability, shared metadata, or concurrent operation. Spring Batch 6 also provides a resourceless repository for a single one-time job, but the repository documentation states that it stores no metadata and is not thread-safe.
For this import, create the Spring Batch metadata schema and an application table:
create table customer (
customer_id bigint primary key,
email varchar(320) not null,
country_code char(2) not null,
imported_at timestamp not null
);
Production schema initialization belongs to migrations rather than an always-on development setting.
Model the input and output records
Separate the raw CSV record from the validated domain record so the transformation remains easy to test.
public record CsvCustomer(long customerId, String email, String countryCode) {}
public record Customer(long customerId, String email, String countryCode) {}
The input file uses this header:
customer_id,email,country_code
101,ada@example.com,GB
102,grace@example.com,US
Avoid embedding validation, database access, and formatting inside one record mapper. The reader maps syntax, the processor handles business rules, and the writer persists accepted output.
Configure a restartable CSV reader
A restartable CSV reader needs a stable name, a declared header policy, column names, and saved state.
@Bean
@StepScope
FlatFileItemReader<CsvCustomer> customerReader(
@Value("#{jobParameters['inputFile']}") Resource inputFile) {
return new FlatFileItemReaderBuilder<CsvCustomer>()
.name("customerReader")
.resource(inputFile)
.linesToSkip(1)
.delimited()
.names("customerId", "email", "countryCode")
.fieldSetMapper(fieldSet -> new CsvCustomer(
fieldSet.readLong("customerId"),
fieldSet.readString("email"),
fieldSet.readString("countryCode")))
.saveState(true)
.build();
}
@StepScope delays bean creation until a step starts, allowing the runtime inputFile job parameter to bind. The reader stores its position in the step ExecutionContext, so a restart resumes from the last committed checkpoint rather than line 1.
The input file itself also needs stability. Replacing a file between failure and restart can corrupt the result even when metadata is correct. Record a checksum, immutable object-store version, or intake identifier as a job parameter.
Validate and normalize in the processor
The processor returns a clean domain value, returns null to filter intentionally, or throws a typed exception for invalid data.
@Bean
ItemProcessor<CsvCustomer, Customer> customerProcessor() {
return input -> {
String email = input.email().trim().toLowerCase(Locale.ROOT);
String country = input.countryCode().trim().toUpperCase(Locale.ROOT);
if (!email.contains("@")) {
throw new ValidationException("Invalid email for " + input.customerId());
}
if (country.length() != 2) {
throw new ValidationException(
"Invalid country code for " + input.customerId());
}
return new Customer(input.customerId(), email, country);
};
}
Use a domain-specific exception so the step can distinguish a bad record from a database outage. Logging a raw customer record may expose personal data, so include a safe record key and error code instead.
Make the database writer idempotent
Safe restarts depend on an idempotent writer: writing the same business record twice still produces one correct result. A primary key plus an upsert is more reliable than assuming the framework can prevent every external duplicate.
@Bean
JdbcBatchItemWriter<Customer> customerWriter(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<Customer>()
.dataSource(dataSource)
.sql("""
insert into customer (
customer_id, email, country_code, imported_at
) values (
:customerId, :email, :countryCode, current_timestamp
)
on conflict (customer_id) do update set
email = excluded.email,
country_code = excluded.country_code,
imported_at = current_timestamp
""")
.beanMapped()
.build();
}
The Spring Batch step documentation warns that different transaction managers for processing data and repository metadata can permit re-execution after a partial failure. Idempotency protects the destination when one transaction cannot cover both systems.
Assemble the chunk step and job
The import connects the reader, processor, and writer to a transactional chunk step, then places that step in a named job.
@Bean
Step importCustomers(
JobRepository jobRepository,
PlatformTransactionManager transactionManager,
FlatFileItemReader<CsvCustomer> customerReader,
ItemProcessor<CsvCustomer, Customer> customerProcessor,
JdbcBatchItemWriter<Customer> customerWriter) {
return new StepBuilder("importCustomers", jobRepository)
.<CsvCustomer, Customer>chunk(100)
.transactionManager(transactionManager)
.reader(customerReader)
.processor(customerProcessor)
.writer(customerWriter)
.faultTolerant()
.skip(ValidationException.class)
.skipLimit(25)
.build();
}
@Bean
Job customerImportJob(JobRepository jobRepository, Step importCustomers) {
return new JobBuilder("customerImportJob", jobRepository)
.start(importCustomers)
.build();
}
The current chunk configuration reference attaches the transaction manager to the chunk builder. Older tutorials often use builder factories or signatures from earlier releases, so compare copied code with the documentation for the installed version.
Treat chunk size as a transaction decision
Chunk size is a transaction decision, so measure database throughput, memory, lock duration, rollback cost, and downstream limits. A larger chunk reduces commit overhead but increases the work repeated after a rollback.
For a 10-million-row import, chunk(100) does not mean only 100 records process. It means each transaction attempts 100 items. The job continues chunk by chunk until the reader reaches the end.
Load-test several values with representative records. Record read count, write count, skip count, commit count, rollback count, duration, and database pressure. The fastest standalone benchmark can still be the wrong setting if it holds locks long enough to affect other applications.
Distinguish skip, retry, rollback, and failure
Fault tolerance starts by classifying exceptions according to whether another attempt can change the result.
- Skip a permanently invalid item, such as a malformed country code, within a reviewed limit.
- Retry a transient operation, such as a deadlock or temporary service timeout.
- Fail on unknown bugs, broken schemas, or exceeded limits.
- Prevent rollback only when transaction behavior and side effects are fully understood.
The Spring Batch retry documentation contrasts deterministic parse failures with transient database deadlocks. Retrying malformed CSV wastes time; skipping a database outage hides a system failure.
.faultTolerant()
.retry(DeadlockLoserDataAccessException.class)
.retryLimit(3)
.skip(ValidationException.class)
.skipLimit(25)
Write skipped records to a controlled rejection report through a listener, with the safe source identifier, reason code, step execution ID, and timestamp. Do not expose secrets or full personal records in logs.
Design job parameters for identity and restart
Predictable job launches separate identifying parameters from operational values. The job name plus identifying parameters defines the JobInstance.
Useful identifying parameters include:
businessDate=2026-09-07inputFile=/imports/customers-2026-09-07.csvinputChecksum=...
Adding a random run.id creates a new instance rather than restarting the failed one. That is correct for intentional reruns and wrong for recovery. The job configuration reference explains that an existing instance plus another execution is treated as a restart.
Verify restart behavior with a failure drill
Restartability is easiest to trust after a deliberate failure drill. Force a failure after several committed chunks, restart with identical identifying parameters, and inspect destination rows plus repository metadata.
The drill needs to verify:
- Earlier committed chunks remain present.
- The reader resumes from saved state.
- The writer does not create duplicates.
- Read, write, skip, commit, and rollback counts make sense.
- A completed instance cannot accidentally run again with the same identity.
For Spring Batch 6, use JobOperatorTestUtils. The API marks JobLauncherTestUtils deprecated in 6.0 in favor of the operator-based utility.
@SpringBatchTest
@SpringBootTest
class CustomerImportJobTest {
@Autowired
JobOperatorTestUtils jobOperatorTestUtils;
@Test
void importsValidRows() throws Exception {
JobParameters parameters = new JobParametersBuilder()
.addString("inputFile", "classpath:customers-test.csv")
.addLocalDate("businessDate", LocalDate.of(2026, 9, 7))
.toJobParameters();
JobExecution execution = jobOperatorTestUtils.startJob(parameters);
assertThat(execution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
assertThat(execution.getStepExecutions())
.singleElement()
.satisfies(step -> assertThat(step.getWriteCount()).isEqualTo(2));
}
}
Test the reader and processor separately for parsing boundaries and business rules. Keep one end-to-end test for the job flow, metadata, and transaction behavior.
Add observability before scaling
Operational monitoring covers status, duration, throughput, skip count, retry count, rollback count, and the last successful business date. Spring Batch stores core execution statistics in the repository and integrates with application observability.
Alert on conditions that affect business completion:
- no successful run by the expected cutoff;
- a running execution with no progress;
- a skip ratio above the agreed threshold;
- repeated retries or rollbacks;
- input count far outside its historical range;
- destination reconciliation mismatch.
Logs explain individual failures. Metrics show trends. Repository metadata provides the authoritative execution history. Keep all three connected with job and step execution IDs.
Scale only after measuring the bottleneck
Profile reading, processing, writing, network calls, locks, and commit cost before enabling parallelism. Spring Batch supports multi-threaded steps, parallel flows, partitioning, and remote chunking, but each changes ordering and failure behavior.
| Strategy | Best fit | Main constraint |
|---|---|---|
| Larger chunks | Commit overhead dominates | Rollback cost and lock duration |
| Multi-threaded step | Processor work is independent | Reader and writer thread safety |
| Parallel steps | Independent flows exist | Shared resource contention |
| Partitioning | Data divides by stable key ranges | Balanced partitions and idempotency |
| Remote chunking | Processing distributes across workers | Messaging, ordering, and operational complexity |
Do not add threads to a non-thread-safe reader. Do not partition on ranges that create one huge hot partition. Include the chosen scaling strategy in reconciliation and restart tests.
Use a production readiness checklist
Before release, verify behavior across normal, invalid, duplicate, empty, partial, and restarted inputs.
- Pin the Spring Boot and Spring Batch versions through dependency management.
- Manage metadata and application schemas through migrations.
- Define identifying parameters and immutable input identity.
- Make every external write idempotent.
- Set skip and retry policies for named exception types.
- Protect personal data in logs and rejection files.
- Test a forced failure after committed chunks.
- Reconcile source, written, filtered, and skipped counts.
- Monitor job completion and business outcomes.
- Document how operators stop, restart, and rerun the job.
Students can use Java assignment help to review the configuration, computer science homework help to structure the full submission, or online programming tutoring to trace the execution lifecycle before a code defense. A batch job is complete only when its failure path is as deliberate as its success path.
Questions about Spring Batch
What is Spring Batch used for?
Spring Batch handles finite bulk-processing jobs such as imports, exports, billing runs, reconciliation, report generation, and transactional ETL workflows.
Does Spring Batch schedule jobs?
No. Spring Batch executes and manages batch work. A scheduler such as Quartz, Kubernetes CronJob, or an enterprise scheduler decides when to launch it.
What is the difference between a Job and a Step?
A Job defines the complete batch flow. A Step defines one phase within that flow, such as validating a file or importing its records.
What does chunk size mean?
Chunk size is the number of items attempted before a transaction commits. It controls commit frequency, rollback exposure, memory behavior, and often throughput.
How does a failed Spring Batch job restart?
The repository loads the previous execution metadata and step ExecutionContext. A restart with the same job identity resumes incomplete restartable steps from their saved checkpoints.
When is a tasklet better than chunk processing?
A tasklet fits one bounded action such as deleting a file or calling a procedure once. Chunk processing fits repeated read-process-write work across many items.
What is the difference between skip and retry?
Skip accepts that a specific item cannot process and continues within a limit. Retry repeats an operation because a transient condition may clear.
Why does a Spring Batch writer need idempotency?
Failures across separate systems can cause an item or chunk to execute again. An idempotent writer turns repeated delivery of the same business record into one correct destination state.
References
Related articles
-
JavaAdvanced Java Data Management Techniques
Master advanced Java data management: optimize data structures, handle concurrent access, tune memory, and use serialization and compression in real applications.
May 3, 2024
-
JavaJava File I/O: Read, Write, and Manage Files
A practical guide to Java file I/O: streams, readers and writers, NIO Path and Files, buffering, serialization, and the exceptions that break file code.
Oct 7, 2023
-
JavaException Handling in Java: Full Guide
How exception handling in Java works: checked vs unchecked, try-catch-finally, custom exceptions, try-with-resources, and the mistakes to avoid.
Sep 25, 2023