๐ฆ Ultimate Guide to Conditional Flow in Spring Batch (Deep Dive)
Spring Batch powers large-scale enterprise workloads and often needs to make **dynamic decisions**:
- Should Step B run after Step A?
- Should we retry, skip, or branch?
- Should we trigger an alternate flow on data failure?
- Should we route based on job parameters?
This is where **Conditional Flow** becomes essential. In this ultimate guide, we explore everything from basic transitions to complex multi-flow architectures.
๐ What is Conditional Flow?
Conditional flow lets your batch job **behave like a workflow**, choosing different paths based on:
- ExitStatus of a Step
- JobExecutionDecider with custom logic
- External data or job parameters
- Failures, skips, warning states
- Parallel execution (split flows)
// High-Level Flow Diagram
[Step A]
|
| COMPLETED
v
[Step B]
|
| FAILED
v
[Error Handler Step]
๐ฆ Maven 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>
๐งฑ Defining Steps
We define three simple tasklet steps for demonstration.
@Bean
public Step startStep() {
return stepBuilderFactory.get("startStep")
.tasklet((c, ctx) -> {
System.out.println("Running Start Step");
return RepeatStatus.FINISHED;
}).build();
}
@Bean
public Step successStep() {
return stepBuilderFactory.get("successStep")
.tasklet((c, ctx) -> {
System.out.println("Running Success Step");
return RepeatStatus.FINISHED;
}).build();
}
@Bean
public Step errorStep() {
return stepBuilderFactory.get("errorStep")
.tasklet((c, ctx) -> {
System.out.println("Running Error Step");
return RepeatStatus.FINISHED;
}).build();
}
๐ง Understanding Step ExitStatus vs FlowExecutionStatus
| Type | Used Where? | Purpose |
|---|---|---|
| ExitStatus | Step Execution | Determines if step succeeded (COMPLETED), failed (FAILED), etc. |
| FlowExecutionStatus | Deciders | Routes next step based on business logic (SUCCESS, ERROR, etc.) |
๐ง Creating a Custom JobExecutionDecider
@Component
public class RandomDecider implements JobExecutionDecider {
@Override
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
boolean status = new Random().nextBoolean();
return status
? new FlowExecutionStatus("SUCCESS")
: new FlowExecutionStatus("ERROR");
}
}
๐งฑ Basic Conditional Job Flow
@Bean
public Job conditionalJob() {
return jobBuilderFactory.get("conditionalJob")
.start(startStep())
.next(decider())
.on("SUCCESS").to(successStep())
.from(decider())
.on("ERROR").to(errorStep())
.end()
.build();
}
@Bean
public JobExecutionDecider decider() {
return new RandomDecider();
}
๐ Conditional Flow Based on ExitStatus
You can route steps purely based on ExitStatus without a decider:
@Bean
public Job exitStatusJob() {
return jobBuilderFactory.get("exitStatusJob")
.start(stepA())
.on("COMPLETED").to(stepB())
.from(stepA())
.on("FAILED").to(failureHandler())
.end()
.build();
}
๐งฉ Conditional Flow Based on Job Parameters
Often you want to route steps based on input parameters:
@Component
public class ParameterBasedDecider implements JobExecutionDecider {
@Override
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
String mode = jobExecution.getJobParameters().getString("mode");
return "FULL".equalsIgnoreCase(mode)
? new FlowExecutionStatus("FULL_LOAD")
: new FlowExecutionStatus("DELTA_LOAD");
}
}
๐ Splitting the Job Into Parallel Flows
Spring Batch supports **parallel flows** using `split()`.
@Bean
public Job parallelJob() {
Flow flow1 = new FlowBuilder<Flow>("flow1")
.start(step1())
.next(step2())
.build();
Flow flow2 = new FlowBuilder<Flow>("flow2")
.start(step3())
.build();
return jobBuilderFactory.get("parallelJob")
.start(flow1)
.split(new SimpleAsyncTaskExecutor()).add(flow2)
.end()
.build();
}
๐งฑ Nested Conditional Flows (Advanced)
@Bean
public Job nestedFlowJob() {
Flow innerFlow = new FlowBuilder<Flow>("innerFlow")
.start(stepA())
.next(stepB())
.build();
return jobBuilderFactory.get("nestedFlowJob")
.start(startStep())
.next(decider())
.on("RUN_INNER").to(innerFlow)
.from(decider())
.on("SKIP_INNER").to(stepC())
.end()
.build();
}
๐งจ Common Mistakes & How to Avoid Them
- ❌ Using `ExitStatus` when custom logic is required → Use Decider
- ❌ Forgetting `.end()` after multi-step transitions
- ❌ Reusing the same decider instance inside multiple flows
- ❌ Complex routing inside steps (keep business logic out of step)
๐ง Real-World Use Cases
- ๐ **Reprocessing only failed records** (using param-based decider)
- ๐ **Full load vs delta load routing**
- ๐ **Switching flow when a file is missing**
- ๐ **Different flows for weekday/weekend processing**
๐ Trigger Job Using REST
@RestController
@RequestMapping("/jobs")
public class JobController {
@Autowired private JobLauncher launcher;
@Autowired private Job conditionalJob;
@GetMapping("/run")
public String run() throws Exception {
JobParameters params = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.addString("mode", "FULL")
.toJobParameters();
launcher.run(conditionalJob, params);
return "Job Started!";
}
}
✅ Summary
- Conditional Flow makes Spring Batch behave like a workflow engine.
- Use ExitStatus for simple transitions.
- Use JobExecutionDecider for complex decisions.
- Parallel and nested flows help solve advanced scenarios.
- Job parameters allow dynamic routing at runtime.
๐บ Want to learn Spring with hands-on videos?
Subscribe to our YouTube channel: Spring Java Lab for practical Spring Batch tutorials!
Subscribe to our YouTube channel: Spring Java Lab for practical Spring Batch tutorials!