Ultimate Guide: Conditional Flow in Spring Batch

๐Ÿšฆ 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!