๐ฆ 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.
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". |
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.