With the release of Spring Boot 4.0 and Spring Framework 7.0, the batch processing landscape has evolved to embrace Java 25, Jakarta EE 11, and built-in resilience patterns. This guide provides a professional architectural blueprint for setting up a high-performance Spring Batch server.

1. Technical Baseline

  • Java: 17 (Baseline) / 25 (Recommended)
  • Build Tool: Gradle 9.x
  • Frameworks: Spring Boot 4.0.0+, Spring Framework 7.0.0+
  • Persistence: Jakarta Persistence (JPA) 3.2
  • Batch Version: Spring Batch 6.0

2. Dependency Management (build.gradle)

Spring Boot 4.0 introduces a modularized codebase. We leverage the spring-boot-starter-batch which now targets the Jakarta EE 11 namespace.

plugins {
    id 'java'
    id 'org.springframework.boot' version '4.0.0'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.architect.batch'
version = '1.0.0'
java.sourceCompatibility = JavaVersion.VERSION_25

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    
    // Spring AI Integration for LLM-based batch tasks
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'

    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.batch:spring-batch-test'
}

3. Core Architectural Changes

The Removal of @EnableBatchProcessing

In Spring Boot 4.0, @EnableBatchProcessing is no longer required for default auto-configuration. Instead, the framework provides a DefaultBatchConfiguration that you can extend if you need deep customization.

Modernizing the Builder API

Spring Batch 6.0 (bundled with Boot 4.0) has finalized the removal of JobBuilderFactory and StepBuilderFactory.

  • JobBuilder: Now requires a JobRepository explicitly passed to the constructor. This ensures that the job’s state management is clearly defined at the point of instantiation.
  • StepBuilder: Requires both a JobRepository and, for chunk-oriented steps, a PlatformTransactionManager. This explicit injection supports better testing and multi-transaction-manager environments.

4. Understanding the Builders: Orchestration vs. Execution

To design a robust batch system, it is critical to understand the functional hierarchy between the JobBuilder and the StepBuilder.

The Functional Purpose of JobBuilder

The JobBuilder acts as the Orchestrator. Its primary responsibility is to define the high-level workflow and the lifecycle of the entire batch process.

  • Scope: It manages global concerns such as job parameters, listeners, and step sequencing.
  • Flow Control: Defines transitions—whether the job proceeds linearly, forks, or stops.
  • State Management: Tracks “Job Instance” vs “Job Execution” for restartability via the JobRepository.

The Functional Purpose of StepBuilder

The StepBuilder acts as the Executor. It defines the “how” of the data processing logic within a specific phase.

  • Granularity: The independent unit of work.
  • Processing Paradigms: Chunk-based (Reader/Processor/Writer) or Tasklet-based.
  • Isolation: Encapsulates the transactional boundary for its work.

5. Advanced Workflow Orchestration

As an architect, you often need more than a simple linear chain. Spring Batch provides a powerful DSL for conditional logic and branching.

Chaining and Forking (Conditional Flow)

You can use .on(), .to(), and .from() to create complex state machines.

@Bean
public Job complexWorkflowJob(JobRepository jobRepository, 
                             Step validationStep, 
                             Step aiProcessingStep, 
                             Step manualReviewStep,
                             Step archiveStep) {
    return new JobBuilder("complexWorkflowJob", jobRepository)
            .start(validationStep)
                .on("FAILED").to(manualReviewStep) // Fork if validation fails
            .from(validationStep)
                .on("COMPLETED").to(aiProcessingStep) // Continue if success
            .from(aiProcessingStep)
                .on("*").to(archiveStep) // Default transition
            .end()
            .build();
}

Stopping and Failing Jobs

You can explicitly control the final state of a job based on business logic:

  • stop(): Transitions the job to a STOPPED state. It can be restarted from this point.
  • fail(): Transitions the job to FAILED. It marks the execution as a failure but allows for restart depending on configuration.
  • end(): Marks the job as COMPLETED.
@Bean
public Job conditionalStopJob(JobRepository jobRepository, Step step1, Step cleanupStep) {
    return new JobBuilder("conditionalStopJob", jobRepository)
            .start(step1)
                .on("NO_DATA_FOUND").stop() // Graceful stop if no work found
            .from(step1)
                .on("CRITICAL_ERROR").fail() // Explicit failure
            .from(step1)
                .on("*").to(cleanupStep)
            .end()
            .build();
}

Parallel Execution (Splitting)

To maximize throughput on Java 25, you can execute independent flows in parallel using split.

@Bean
public Job parallelJob(JobRepository jobRepository, Flow flow1, Flow flow2, Step finalMergeStep) {
    return new JobBuilder("parallelJob", jobRepository)
            .start(flow1)
            .split(new SimpleAsyncTaskExecutor()) // Runs flow1 and flow2 in parallel
            .add(flow2)
            .next(finalMergeStep)
            .end()
            .build();
}

6. Implementation: The LLM Data Processor (Spring AI)

@Component
public class AIEnrichmentProcessor implements ItemProcessor<InputData, EnrichedData> {
    private final ChatClient chatClient;

    public AIEnrichmentProcessor(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @Override
    public EnrichedData process(InputData item) {
        String analysis = chatClient.prompt()
                .user("Analyze this record: " + item.getContent())
                .call()
                .content();
        return new EnrichedData(item.getId(), analysis);
    }
}

7. Job Configuration (Modern Style)

@Configuration
public class BatchServerConfiguration {
    @Bean
    public Job enrichmentJob(JobRepository jobRepository, Step mainStep) {
        return new JobBuilder("enrichmentJob", jobRepository)
                .start(mainStep)
                .build();
    }

    @Bean
    public Step mainStep(JobRepository jobRepository, 
                        PlatformTransactionManager transactionManager,
                        AIEnrichmentProcessor processor) {
        return new StepBuilder("mainStep", jobRepository)
                .<InputData, EnrichedData>chunk(10, transactionManager)
                .reader(new CustomItemReader()) 
                .processor(processor)
                .writer(new CustomItemWriter()) 
                .build();
    }
}

8. Automating Job Execution (Scheduling)

@Configuration
@EnableScheduling
public class SchedulingConfiguration {}

@Service
public class BatchJobScheduler {
    private final JobLauncher jobLauncher;
    private final Job enrichmentJob;

    public BatchJobScheduler(JobLauncher jobLauncher, Job enrichmentJob) {
        this.jobLauncher = jobLauncher;
        this.enrichmentJob = enrichmentJob;
    }

    @Scheduled(cron = "0 0 0 * * ?")
    public void scheduleEnrichmentJob() throws Exception {
        var params = new JobParametersBuilder()
                .addLong("timestamp", System.currentTimeMillis())
                .toJobParameters();
        jobLauncher.run(enrichmentJob, params);
    }
}

9. Resilience & Retries

Spring Framework 7 integrates spring-retry into spring-core. Use @Retryable in your processors to handle transient LLM API failures.

10. REST Controller Integration

@RestController
public class JobTriggerController {
    private final JobLauncher jobLauncher;
    private final Job enrichmentJob;

    public JobTriggerController(JobLauncher jobLauncher, Job enrichmentJob) {
        this.jobLauncher = jobLauncher;
        this.enrichmentJob = enrichmentJob;
    }

    @PostMapping("/jobs/enrichment")
    public void runJob(@RequestParam String referenceId) throws Exception {
        var params = new JobParametersBuilder()
                .addString("referenceId", referenceId)
                .addLong("timestamp", System.currentTimeMillis())
                .toJobParameters();
        jobLauncher.run(enrichmentJob, params);
    }
}

Conclusion

Setting up a Spring Batch server on the 4.0/7.0 stack involves embracing Jakarta EE 11 and Java 25. By leveraging advanced orchestration patterns like conditional forking, parallel splitting, and Spring AI, you build a resilient, modern data processing engine.


Discover more from GhostProgrammer - Jeff Miller

Subscribe to get the latest posts sent to your email.

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.