How to Restart Failed Jobs in Spring Batch
In batch processing, jobs can fail for various reasons, including data issues, network problems, or unexpected exceptions. Restarting a failed job from the beginning can be inefficient, particularly when dealing with large datasets. Spring Batch includes built-in restart capabilities that enable jobs to resume processing from where they stopped, minimising rework and improving efficiency. This article explains how to configure a Spring Batch job to restart after a failure and continue processing.
1. Spring Batch Restart Overview
Restartability in Spring Batch relies on a few core concepts:
- JobRepository – Stores job execution metadata in a database, enabling Spring Batch to track progress between runs.
- JobExecution – Represents a specific run of a job, storing start time, end time, and status.
- StepExecution – Represents a single execution of a step in a job, holding step-specific status.
- ExecutionContext – Stores state information so processing can resume from the last checkpoint.
When restartability is enabled (default behaviour), Spring Batch stores reader/writer state in the ExecutionContext. On job restart, Spring Batch uses this state to pick up from the failure point.
2. Spring Batch Job Configuration – Processing a CSV File
In this section, we define a Spring Batch job with one step: reading from a CSV file, processing data, and writing to the console. The job will be restartable by default.
Maven pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>
This pom.xml defines the dependencies needed for a Spring Batch job. We use spring-boot-starter-batch for batch functionality and H2 as an in-memory database for the JobRepository.
application.properties
spring.datasource.url=jdbc:h2:~/testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.format_sql=true spring.batch.jdbc.initialize-schema=always spring.batch.jdbc.table-prefix=JOB_ spring.sql.init.mode=always # Custom property for CSV file location input.file=src/main/resources/customers.csv
This configuration sets up an in-memory H2 database for Spring Batch metadata storage. initialize-schema: always ensures that Spring Batch creates the required job tables.
Customer.java
public class Customer {
private String firstName;
private String lastName;
private String email;
public Customer() {
}
public Customer(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "Customer{" + "firstName=" + firstName + ", lastName=" + lastName + ", email=" + email + '}';
}
}
Customer is a simple POJO that represents each row from the CSV file. It contains three fields with constructors, getters, and a toString() method for easy output.
Batch Job Configuration Class – BatchConfig.java
The BatchConfig class below is used for the Spring Batch setup. It defines the components required for the job to function, including the ItemReader, ItemProcessor, and ItemWriter. It also registers the job and step definitions, connects them to the JobRepository, and configures chunk-oriented processing.
@Configuration
public class BatchConfig {
@Value("${input.file}")
private String inputFile;
@Bean
@StepScope
public FlatFileItemReader<Customer> reader() {
return new FlatFileItemReaderBuilder<Customer>()
.name("customerItemReader")
.resource(new FileSystemResource(inputFile))
.delimited()
.names("firstName", "lastName", "email")
.targetType(Customer.class)
.saveState(true)
.build();
}
@Bean
public CustomerItemProcessor customerItemProcessor() {
return new CustomerItemProcessor();
}
@Bean
public ItemWriter<Customer> writer() {
return items -> items.forEach(System.out::println);
}
@Bean
public Job importCustomerJob(JobRepository jobRepository, Step step1) {
return new JobBuilder("importCustomerJob", jobRepository)
.start(step1)
.build();
}
@Bean
public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("step1", jobRepository)
.<Customer, Customer>chunk(2, transactionManager)
.reader(reader())
.processor(customerItemProcessor())
.writer(writer())
.build();
}
}
Defining the Item Reader
@Bean
@StepScope
public FlatFileItemReader<Customer> reader() {
return new FlatFileItemReaderBuilder<Customer>()
.name("customerItemReader")
.resource(new FileSystemResource(inputFile))
.delimited()
.names("firstName", "lastName", "email")
.targetType(Customer.class)
.saveState(true)
.build();
}
This method creates a FlatFileItemReader<Customer> that reads customer data from a CSV file. The inputFile path is injected from the application.properties file via @Value("${input.file}"). Using FlatFileItemReaderBuilder, it specifies the file resource, maps CSV columns to the Customer class, and enables state saving so the job can resume from where it left off if restarted.
The @StepScope annotation ensures that this reader is created fresh for each job step execution, allowing dynamic parameter injection at runtime.
Defining the Item Processor
@Bean
public CustomerItemProcessor customerItemProcessor() {
return new CustomerItemProcessor();
}
This bean creates a CustomerItemProcessor instance, which implements the ItemProcessor<Customer, Customer> interface. In its process() method, it can transform or validate each Customer record. The example processor throws an exception if the first name equals "Error" and the failOnError flag is true. This allows you to filter or reject problematic records during processing.
CustomerItemProcessor class
public class CustomerItemProcessor implements ItemProcessor<Customer, Customer> {
private boolean failOnError = true;
public void setFailOnError(boolean failOnError) {
this.failOnError = failOnError;
}
@Override
public Customer process(Customer customer) throws Exception {
if (failOnError && "Error".equalsIgnoreCase(customer.getFirstName())) {
throw new RuntimeException("Error processing: " + customer);
}
return customer;
}
}
Defining the Item Writer
@Bean
public ItemWriter writer() {
return items -> items.forEach(System.out::println);
}
This method defines an ItemWriter<Customer> that outputs each processed Customer to the console.
Defining the Job
@Bean
public Job importCustomerJob(JobRepository jobRepository, Step step1) {
return new JobBuilder("importCustomerJob", jobRepository)
.start(step1)
.build();
}
This bean defines a Job named "importCustomerJob". Using the JobBuilder, it links the job to a JobRepository (which stores execution metadata) and specifies that it should start with step1.
Defining the Step
@Bean
public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
return new StepBuilder("step1", jobRepository)
.chunk(2, transactionManager)
.reader(reader())
.processor(customerItemProcessor())
.writer(writer())
.build();
}
3. Simulating a Job Failure
To test job restart, we create a CSV file where one record contains "Error" as the first name.
customers.csv
John,Thomas,john@example.com Alan,Smith,alan@example.com Error,Fail,error@example.com Alice,Johnson,alice@example.com
The third record will trigger the processor’s simulated failure, causing the job to stop mid-execution. When started with mvn spring-boot:run, the application outputs:
Executing step: [step1]
Customer{firstName=John, lastName=Thomas, email=john@example.com}
Customer{firstName=Alan, lastName=Smith, email=alan@example.com}
...
Encountered an error executing step step1 in job importCustomerJob
java.lang.RuntimeException: Error processing: Customer{firstName=Error, lastName=Fail, email=error@example.com}
...
Step: [step1] executed in 204ms
Job: [SimpleJob: [name=importCustomerJob]] completed with the following parameters:
[{}] and the following status: [FAILED] in 321ms
On the first run, the job begins processing from the first line, successfully handles two records, and then fails on the third record "Error,Fail,error@example.com". This confirms that the batch job started execution, processed customer records, and encountered a failure when a record with the first name "Error" was read.
Spring Batch then stored the job’s progress in its metadata tables (such as BATCH_JOB_EXECUTION and BATCH_STEP_EXECUTION), ensuring the execution state is persisted so the job can be restarted from the point of failure, rather than starting over.
4. Restarting the Job
After the first run fails, the metadata tables contain all the information needed to resume from where the job stopped. To restart the failed job, we implement a CommandLineRunner that detects the last failed JobInstance using the JobExplorer and relaunches it with the same JobParameters.
This approach ensures that Spring Batch processes only the remaining unprocessed records, rather than starting over.
@SpringBootApplication
public class SpringBatchRestartApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBatchRestartApplication.class, args);
}
@Bean
public CommandLineRunner jobRestarter(
JobExplorer jobExplorer,
JobLauncher jobLauncher,
Job job,
CustomerItemProcessor itemProcessor) {
return args -> {
String jobName = job.getName();
List<JobInstance> instances = jobExplorer.getJobInstances(jobName, 0, 1);
if (!instances.isEmpty()) {
JobInstance lastInstance = instances.get(0);
List<JobExecution> executions = jobExplorer.getJobExecutions(lastInstance);
for (JobExecution execution : executions) {
if (execution.getStatus() == BatchStatus.FAILED) {
// Enable ignoring the problematic record on restart
itemProcessor.setFailOnError(false);
JobParameters jobParameters = execution.getJobParameters();
System.out.println("Restarting failed job (skipping bad record) with parameters: " + jobParameters);
jobLauncher.run(job, jobParameters);
return;
}
}
}
JobParameters parameters = new JobParametersBuilder()
.addString("jobId", "batch-job-" + System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(job, parameters);
};
}
}
This CommandLineRunner runs when the application starts. It first retrieves the most recent JobInstance of our batch job, then checks all related JobExecution records for a FAILED status. If a failed execution is found, it restarts the job with the same JobParameters used during the failed run. This ensures Spring Batch resumes processing from the failure point, rather than starting over.
After adding the restart logic and rerunning the application, Spring Batch detects the previously failed job and resumes from the point of failure. This restart mechanism skips already processed records and processes only the remaining ones. In this case, the job continues from the failed chunk, starting with "Error,Fail,error@example.com" and then "Alice,Johnson,alice@example.com", without reprocessing "John" and "Alan".
Running the application again produces:
Executing step: [step1]
Customer{firstName=Error, lastName=Fail, email=error@example.com}
Customer{firstName=Alice, lastName=Johnson, email=alice@example.com}
Step: [step1] executed in 46ms
Job: [SimpleJob: [name=importCustomerJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 148ms
In this run, the batch job starts at the first unprocessed customer record after the failure point from the first run. Spring Batch uses the persisted job metadata to determine where to resume, avoiding reprocessing of already completed items.
5. Conclusion
This article explored how to configure and run a Spring Batch job with a custom ItemProcessor, implement error handling, and enable job restarts to recover from failures. The article demonstrated how Spring Batch resumes processing from the point of failure, ensuring data integrity by skipping already processed records and committing work in manageable chunks. These techniques enable the development of robust batch pipelines that gracefully handle errors and seamlessly resume after interruptions.
6. Download the Source Code
This article covered how to configure Spring Batch to restart after a job failure and continue processing.
You can download the full source code of this example here: spring batch restart job failure continue




