Spring Batch Processing Example

Spring Batch Processing Example in Spring Boot (2026 Guide)

Spring Batch Processing Example in Spring Boot

In this tutorial, you will build a Spring Batch application in Spring Boot that reads employee data from a CSV file, processes it, and saves it to a MySQL database. The job is triggered automatically whenever a new file is dropped into a watched folder, using Java NIO's WatchService.

You will also learn how to expose a REST endpoint for file upload, wire up Job and Step listeners, and configure chunk-oriented processing with FlatFileItemReader and RepositoryItemWriter.

Overview

Spring Batch is a lightweight, comprehensive framework for processing large volumes of data in batches. It provides reusable functions for reading, processing, and writing data, along with transaction management, chunk-oriented processing, retry logic, and job monitoring.

In this example, we build a real-world scenario: a Spring Boot REST API accepts a CSV file upload, saves the file to a local folder, and a background WatchService detects the new file and triggers a Spring Batch job to read and persist the data into MySQL.

Key Features

  • Chunk-oriented processing — reads and writes records in configurable batch sizes (chunk size = 10).
  • FlatFileItemReader — reads CSV files and maps each line to an Employee entity.
  • ItemProcessor — an extension point for transforming data before writing.
  • RepositoryItemWriter — writes entities to MySQL using Spring Data JPA.
  • Job & Step listeners — hooks for logging before and after job/step execution.
  • WatchService integration — automatically triggers the job when a new file is detected.
  • REST file upload — accepts MultipartFile and stores file metadata.
  • MySQL persistence — uses Spring Data JPA with auto-generated schema.

Project Structure

The project follows a clean package structure separating configuration, controller, entity, listener, processor, repository, service, and utility layers.

BatchConfig.java

This is the central configuration class. It defines the FlatFileItemReader, the ItemProcessor, the RepositoryItemWriter, the chunk-oriented Step, and the Job.

package com.example.batch.config;

import java.io.File;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.data.RepositoryItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.LineMapper;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;

import com.example.batch.entity.Employee;
import com.example.batch.listener.JobListener;
import com.example.batch.listener.StepListener;
import com.example.batch.processor.EmployeeProcessor;
import com.example.batch.repository.EmployeeRepository;

@Configuration
@EnableBatchProcessing
public class BatchConfig {

    @Autowired
    private JobBuilderFactory jobBuilderFactory;

    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Autowired
    private EmployeeRepository employeeRepository;

    @Value("${location}")
    private String location;

    private String uploadFile = "";

    @Autowired
    private StepListener stepListener;

    @Autowired
    private JobListener jobListener;

    public FlatFileItemReader<Employee> readDataFromFile() {
        FlatFileItemReader<Employee> itemReader = new FlatFileItemReader();
        FileSystemResource fileResource =
                new FileSystemResource(location + File.separator + uploadFile);

        itemReader.setResource(fileResource);
        itemReader.setName("csvReader");
        itemReader.setLinesToSkip(1);
        itemReader.setLineMapper(lineMapper());
        return itemReader;
    }

    private LineMapper<Employee> lineMapper() {
        DefaultLineMapper<Employee> lineMapper = new DefaultLineMapper();
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setDelimiter(",");
        lineTokenizer.setStrict(false);
        lineTokenizer.setNames("First Name", "Last Name", "Email", "Phone",
                "Gender", "Age", "Job Title", "Years Of Experience",
                "Salary", "Department");

        BeanWrapperFieldSetMapper<Employee> fieldSetMapper =
                new BeanWrapperFieldSetMapper();
        fieldSetMapper.setTargetType(Employee.class);

        lineMapper.setLineTokenizer(lineTokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return lineMapper;
    }

    @Bean
    public EmployeeProcessor employeeProcessor() {
        return new EmployeeProcessor();
    }

    public RepositoryItemWriter<Employee> repositoryItemWriter() {
        RepositoryItemWriter<Employee> repositoryItemWriter =
                new RepositoryItemWriter();
        repositoryItemWriter.setRepository(employeeRepository);
        repositoryItemWriter.setMethodName("save");
        return repositoryItemWriter;
    }

    public Step step1() {
        return stepBuilderFactory.get("step1")
                .<Employee, Employee>chunk(10)
                .reader(readDataFromFile())
                .processor(employeeProcessor())
                .writer(repositoryItemWriter())
                .listener(stepListener)
                .build();
    }

    public Job startJob() {
        return jobBuilderFactory.get("saveEmployee")
                .listener(jobListener)
                .flow(step1())
                .end()
                .build();
    }

    public String getUploadFile() {
        return uploadFile;
    }

    public void setUploadFile(String uploadFile) {
        this.uploadFile = uploadFile;
    }
}

AppController.java

This REST controller exposes a single endpoint to accept CSV file uploads. The file is delegated to FileDetailService, which saves the file metadata and transfers the file to the configured upload directory.

package com.example.batch.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.example.batch.service.FileDetailService;

@RestController
@RequestMapping("/api/v1")
public class AppController {

    @Autowired
    private FileDetailService fileDetailService;

    @PostMapping("/fileUpload")
    public ResponseEntity<Object> uploadFile(@RequestParam("file") MultipartFile file) {
        return fileDetailService.uploadFileDetail(file);
    }
}

Entities — Employee & FileDetailEntity

Two JPA entities are used. Employee stores the employee records read from the CSV. FileDetailEntity stores metadata about each uploaded file, ensuring no file is processed twice.

Employee.java

package com.example.batch.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name = "employee_records")
@Setter
@Getter
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    private String firstName;
    private String lastName;
    private String email;
    private String phone;
    private String gender;
    private int age;
    private String jobTitle;
    private String yearsOfExperience;
    private double salary;
    private String department;
}

FileDetailEntity.java

package com.example.batch.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name = "file_details")
@Setter
@Getter
public class FileDetailEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "file_name", unique = true)
    private String fileName;

    private String uploadDate;
}

JobListener & StepListener

Listeners let you hook into the lifecycle of a job or step. They are useful for logging, metrics, and cleanup operations.

JobListener.java

package com.example.batch.listener;

import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.stereotype.Component;

@Component
public class JobListener implements JobExecutionListener {

    @Override
    public void beforeJob(JobExecution jobExecution) {
        System.out.println("Before Job....");
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        System.out.println("After Job....");
    }
}

StepListener.java

package com.example.batch.listener;

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.stereotype.Component;

@Component
public class StepListener implements StepExecutionListener {

    @Override
    public void beforeStep(StepExecution stepExecution) {
        System.out.println("Before Step....");
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        System.out.println("After Step....");
        return ExitStatus.COMPLETED;
    }
}

WatcherServiceEvent.java

This class implements ApplicationListener<ApplicationReadyEvent>. Once the application starts, it registers a WatchService on the configured folder. Whenever a file is created in that folder, it triggers the Spring Batch job with unique JobParameters.

package com.example.batch.listener;

import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import com.example.batch.config.BatchConfig;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class WatcherServiceEvent implements ApplicationListener<ApplicationReadyEvent> {

    @Value("${location}")
    private String location;

    @Autowired
    private BatchConfig batchConfig;

    @Autowired
    private JobLauncher jobLauncher;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.info("Watcher service enable....");
        watcherService();
    }

    private void watcherService() {
        Path directory = Paths.get(location);
        try (WatchService watchService =
                     FileSystems.getDefault().newWatchService()) {

            directory.register(watchService,
                    StandardWatchEventKinds.ENTRY_CREATE);

            while (true) {
                WatchKey key = watchService.take();

                for (WatchEvent<?> event : key.pollEvents()) {
                    if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                        Path filePath = directory.resolve((Path) event.context());
                        log.info("file " + filePath.getFileName());

                        if (filePath.getFileName().toString()
                                .equals(event.context().toString())) {
                            startJob(event.context().toString());
                        }
                    }
                }
                key.reset();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private BatchStatus startJob(String uploadFileDetail) {
        JobParameters parameters = new JobParametersBuilder()
                .addLong("StartAt", System.currentTimeMillis())
                .toJobParameters();

        JobExecution jobExecution = null;
        try {
            batchConfig.setUploadFile(uploadFileDetail);
            jobExecution = jobLauncher.run(batchConfig.startJob(), parameters);

            if (jobExecution.getStatus().name()
                    .equalsIgnoreCase("COMPLETED")) {
                log.info("Completed.....");
            }
        } catch (Exception e) {
            log.info("Failed...");
            e.printStackTrace();
        }
        return jobExecution.getStatus();
    }
}

EmployeeProcessor.java

The ItemProcessor is where you would add business logic — validation, enrichment, or transformation. In this example, it simply passes the employee through.

package com.example.batch.processor;

import org.springframework.batch.item.ItemProcessor;
import com.example.batch.entity.Employee;

public class EmployeeProcessor implements ItemProcessor<Employee, Employee> {

    @Override
    public Employee process(Employee employee) throws Exception {
        return employee;
    }
}

Repositories

EmployeeRepository.java

package com.example.batch.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.batch.entity.Employee;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}

FileDetailRepository.java

package com.example.batch.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.batch.entity.FileDetailEntity;

@Repository
public interface FileDetailRepository extends JpaRepository<FileDetailEntity, Integer> {
}

FileDetailService.java

The service saves file metadata to the database, then transfers the uploaded file to the configured folder using MultipartFile.transferTo().

package com.example.batch.service;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.example.batch.entity.FileDetailEntity;
import com.example.batch.repository.FileDetailRepository;
import com.example.batch.utility.ResponseHandler;

@Service
public class FileDetailService {

    @Value("${location}")
    private String location;

    @Autowired
    private FileDetailRepository fileDetailRepository;

    private SimpleDateFormat simpleDateFormat =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public ResponseEntity<Object> uploadFileDetail(MultipartFile file) {
        try {
            FileDetailEntity fileDetailEntity = new FileDetailEntity();
            fileDetailEntity.setFileName(file.getOriginalFilename());
            fileDetailEntity.setUploadDate(
                    simpleDateFormat.format(new Date()));

            fileDetailRepository.save(fileDetailEntity);

            file.transferTo(new File(location + File.separator
                    + file.getOriginalFilename()));

            return ResponseHandler.generateResponse(
                    "Successfully File upload", HttpStatus.OK, null);
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseHandler.generateResponse(
                    e.getMessage(), HttpStatus.MULTI_STATUS, null);
        }
    }
}

ResponseHandler.java

A simple utility that builds a consistent JSON response with a message, status code, and data payload.

package com.example.batch.utility;

import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

public class ResponseHandler {

    public static ResponseEntity<Object> generateResponse(
            String message, HttpStatus status, Object responseObj) {

        Map<String, Object> map = new HashMap();
        map.put("message", message);
        map.put("status", status.value());
        map.put("data", responseObj);

        return new ResponseEntity<Object>(map, status);
    }
}

SpringBatchExampleApplication.java

The Spring Boot entry point. Standard @SpringBootApplication class.

package com.example.batch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBatchExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBatchExampleApplication.class, args);
    }
}

application.properties

Configure the database, Spring Batch initialization, and the folder location that the WatchService monitors.

server.port=9090

# DATABASE CONFIGURATION
spring.datasource.url=jdbc:mysql://localhost:3306/batchtutorial
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update

# BATCH CONFIGURATION
spring.batch.initialize-schema=always
spring.batch.job.enabled=false

# UPLOAD FOLDER
location=F://upload

Key settings explained:

  • spring.batch.initialize-schema=always — creates the Spring Batch metadata tables automatically.
  • spring.batch.job.enabled=false — prevents Spring Boot from auto-running jobs at startup so the WatchService can trigger them manually.
  • location=F://upload — the folder monitored by WatchService.

pom.xml

The Maven configuration for the Spring Boot 2.1.3 application with Spring Batch, Web, JPA, Lombok, and MySQL dependencies.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
    https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>Spring-Batch-Example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Spring-Batch-Example</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Postman Request

Use Postman (or any REST client) to upload a CSV file via a POST request with multipart/form-data. The endpoint accepts a form field named file.

Request details:

  • Method: POST
  • URL: http://localhost:9090/api/v1/fileUpload
  • Body type: form-data
  • Key: file (type: File)
  • Value: your CSV file

Database Tables

After the batch job completes, Spring Batch's metadata tables and your custom tables are populated in MySQL.

Tables Created

Final Employee Records

How It Works (Step-by-Step)

  1. Application starts — WatcherServiceEvent is triggered by ApplicationReadyEvent and registers a WatchService on the folder specified in application.properties.
  2. Client uploads a CSV file via POST /api/v1/fileUpload. The file's metadata (name, upload date) is saved to file_details, then the file is transferred to the monitored folder.
  3. WatchService detects the new file via ENTRY_CREATE event and calls startJob().
  4. Job launcher runs the batch job with unique JobParameters (using System.currentTimeMillis() to avoid duplicate job instances).
  5. FlatFileItemReader reads the CSV file line by line, skipping the header, and maps each line to an Employee object.
  6. EmployeeProcessor processes each employee (currently a pass-through, but you can add validation or transformation here).
  7. RepositoryItemWriter saves each processed employee to the employee_records table via Spring Data JPA.
  8. Chunk processing — records are committed in chunks of 10 for optimal performance and transaction management.
  9. Job and Step listeners log messages before and after execution, useful for monitoring.

Frequently Asked Questions

What is Spring Batch and why is it used?

Spring Batch is a lightweight framework for processing large volumes of data in batches. It is used for tasks such as reading data from CSV files, transforming records, and writing them to a database with transaction management, retry logic, and job monitoring.

What is the difference between a Job and a Step in Spring Batch?

A Job is the top-level container for a batch process — it may contain one or more Steps. A Step is a single phase of the job with its own reader, processor, and writer. Chunk-oriented steps process data in configurable batch sizes.

What does FlatFileItemReader do in Spring Batch?

FlatFileItemReader reads flat files such as CSV or fixed-width files. It uses a LineMapper to convert each line into a Java object. It can skip header lines, configure delimiters, and map columns to entity fields.

How do I trigger a Spring Batch job automatically when a file is uploaded?

Use Java NIO's WatchService to monitor a directory for ENTRY_CREATE events. When a file is created, obtain the JobLauncher, build JobParameters, and invoke jobLauncher.run(job, parameters) to trigger the batch job automatically.

Why is spring.batch.job.enabled set to false?

Setting spring.batch.job.enabled=false prevents Spring Boot from automatically running all defined jobs at startup. This allows you to trigger jobs manually via JobLauncher or from a REST endpoint, giving you full control over execution.

Conclusion

In this tutorial, you built a complete Spring Batch application in Spring Boot that processes CSV files automatically using a WatchService. You learned how to configure a chunk-oriented step with FlatFileItemReader, an ItemProcessor, and a RepositoryItemWriter, plus how to hook into the job lifecycle with listeners.

You can extend this project with retry logic, skip policies, partitioning for parallel processing, or scheduling via Quartz. Spring Batch scales well from small CSV files to millions of records.

Related topics: Spring Batch Partitioning, Spring Batch Retry & Skip, JobRepository Configuration, Quartz Scheduler with Spring Batch, Spring Batch with MongoDB, Spring Batch ItemWriter Options

No comments: