Ultimate Guide: Conditional Flow in Spring Batch 5

๐Ÿšฆ Ultimate Guide to Conditional Flow in Spring Batch (Deep Dive)

Most beginner Spring Batch tutorials demonstrate a strictly linear pipeline: Step A executes, then Step B executes, then Step C executes. However, large-scale enterprise workloads rarely operate in a straight line.

What if you only want to run Step B if a specific JobParameter was passed? What if Step A encounters malformed data, and instead of crashing the job, you want to route the execution to a specialized "Error Handling Step"?

This is where Conditional Flow becomes essential. In this ultimate guide, updated for Spring Boot 3 and Spring Batch 5, we explore everything from basic ExitStatus transitions to advanced custom Deciders.

๐Ÿ“บ Visual Learner? Master Spring Batch on YouTube!
Learn how to build, scale, and debug complex Spring Batch architectures by watching my complete, hands-on video series.

▶ Watch the Spring Batch Masterclass Playlist

1. Understanding Step ExitStatus vs. FlowExecutionStatus

Before writing code, you must understand the difference between the two primary status objects used for routing in Spring Batch. Mixing these up is the #1 cause of conditional routing bugs.

Status Type Where is it generated? What is its purpose?
ExitStatus Generated automatically by a Step when it finishes. Indicates the technical outcome of a step (e.g., COMPLETED, FAILED). Used for simple, direct routing.
FlowExecutionStatus Generated manually by a JobExecutionDecider. Used for complex business logic routing (e.g., checking external APIs, JobParameters, or file existence) to return custom statuses like "RUN_DELTA" or "RUN_FULL".
Spring Batch 5 Alert: In Spring Batch 4, we used StepBuilderFactory and JobBuilderFactory. These have been deprecated and removed. All examples below use the modern Spring Batch 5 syntax utilizing JobRepository and PlatformTransactionManager.

2. Basic Conditional Flow Using ExitStatus

The simplest form of conditional routing relies on the ExitStatus of the previous step. In this configuration, if stepA completes successfully, we route to stepB. If it fails, we catch the failure and route to an errorRecoveryStep.

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ExitStatusFlowConfig {

    @Bean
    public Job basicConditionalJob(JobRepository jobRepository, 
                                   Step stepA, 
                                   Step stepB, 
                                   Step errorRecoveryStep) {
                                   
        return new JobBuilder("basicConditionalJob", jobRepository)
                .start(stepA)
                .on("COMPLETED").to(stepB)  // If Step A succeeds, go to Step B
                .from(stepA)
                .on("FAILED").to(errorRecoveryStep) // If Step A fails, go to Recovery
                .end()
                .build();
    }
}

3. Creating a Custom JobExecutionDecider

Routing by ExitStatus is limited. What if Step A completes successfully, but you want to check a database flag to decide whether to run Step B or Step C?

To separate business logic from your Step execution, you implement a JobExecutionDecider.

import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.job.flow.FlowExecutionStatus;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.stereotype.Component;

@Component
public class ParameterBasedDecider implements JobExecutionDecider {

    @Override
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        
        // Extracting a parameter passed dynamically at runtime
        String mode = jobExecution.getJobParameters().getString("mode", "DELTA");

        if ("FULL".equalsIgnoreCase(mode)) {
            return new FlowExecutionStatus("FULL_LOAD");
        } else {
            return new FlowExecutionStatus("DELTA_LOAD");
        }
    }
}

4. Wiring the Decider into the Job Flow

Now we apply our custom Decider to the Job configuration. Notice how readable the Job orchestration becomes when we separate the decision logic from the steps themselves.

@Configuration
public class DeciderFlowConfig {

    @Bean
    public Job deciderConditionalJob(JobRepository jobRepository, 
                                     Step initializationStep,
                                     ParameterBasedDecider decider,
                                     Step fullLoadStep,
                                     Step deltaLoadStep) {

        return new JobBuilder("deciderConditionalJob", jobRepository)
                .start(initializationStep)
                .next(decider) // Pass control to our custom decider
                    .on("FULL_LOAD").to(fullLoadStep)
                .from(decider) // Evaluate the decider again for other outcomes
                    .on("DELTA_LOAD").to(deltaLoadStep)
                .end()
                .build();
    }
}

5. Splitting the Job Into Parallel Flows

Conditional flow isn't just about branching; it's also about concurrency. If Step B and Step C do not depend on each other, you can use a Split flow to run them simultaneously, drastically reducing your total job execution time.

import org.springframework.batch.core.job.builder.FlowBuilder;
import org.springframework.batch.core.job.flow.Flow;
import org.springframework.core.task.SimpleAsyncTaskExecutor;

@Configuration
public class ParallelFlowConfig {

    @Bean
    public Job parallelJob(JobRepository jobRepository, Step step1, Step step2, Step step3) {

        // Define a sub-flow containing step 1 and step 2 sequentially
        Flow flow1 = new FlowBuilder<Flow>("flow1")
                .start(step1)
                .next(step2)
                .build();

        // Define a sub-flow containing only step 3
        Flow flow2 = new FlowBuilder<Flow>("flow2")
                .start(step3)
                .build();

        // Execute Flow 1 and Flow 2 in parallel using an AsyncTaskExecutor
        return new JobBuilder("parallelJob", jobRepository)
                .start(flow1)
                .split(new SimpleAsyncTaskExecutor())
                .add(flow2)
                .end()
                .build();
    }
}

6. Triggering Dynamic Flows via REST

To see our ParameterBasedDecider in action, we need a way to pass dynamic parameters into the job. Here is a REST Controller that allows us to trigger the job and specify whether we want a "FULL" or "DELTA" execution.

import org.springframework.batch.core.Job;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/jobs")
public class JobController {

    @Autowired 
    private JobLauncher jobLauncher;
    
    @Autowired 
    private Job deciderConditionalJob;

    @GetMapping("/run")
    public String runJob(@RequestParam(defaultValue = "DELTA") String mode) throws Exception {
        
        JobParameters params = new JobParametersBuilder()
                .addLong("executionTime", System.currentTimeMillis())
                .addString("mode", mode) // Passing the parameter for the Decider
                .toJobParameters();

        jobLauncher.run(deciderConditionalJob, params);
        
        return "Job Triggered with mode: " + mode;
    }
}

Summary

Conditional Flow transforms Spring Batch from a simple script runner into a robust workflow engine.

  • Use ExitStatus (on("FAILED")) for simple success/fail routing directly from a Step.
  • Use a JobExecutionDecider to pull business logic out of your steps and route based on dynamic data or JobParameters.
  • Use Split Flows to execute independent chains of steps in parallel to optimize processing time.

๐Ÿ”€ Related Spring Batch Tutorials

Explore related Spring Batch concepts to better understand job flows, step execution, error handling, and performance optimization.

๐Ÿงฑ Spring Batch Core Components

Understand Job, Step, ItemReader, ItemProcessor, and ItemWriter — the foundation required before implementing conditional job flows.

⚙️ Spring Batch Tasklet

Learn when to use Tasklet-based steps versus chunk-oriented processing within your branching conditional logic.

๐Ÿงต Multithreaded Step in Spring Batch

Take your Parallel Split flows to the next level by multithreading the actual chunk processing inside the steps.

๐Ÿ” Spring Batch Retry Mechanism

Instead of routing to an Error Step immediately, learn how to handle transient network failures using Retry and Backoff strategies.

๐Ÿšซ Skip Policy & Error Handling

Learn how skip policies work alongside conditional flows to control execution when specific database records fail validation.

๐Ÿ‘‚ JobExecutionListener

Track job status, step outcomes, and decider results by intercepting the job lifecycle before and after execution.