skip to content

What is a JobExecutionDecider in Spring Batch and why would you use one instead of relying on a step's ExitStatus?

level: juniorimportance: must knowfreq 55%

answer

  1. decide(JobExecution, StepExecution) -> FlowExecutionStatus
  2. branch on custom logic, not just pass/fail
  3. .next(decider).on("NAME").to(step)
  4. returns FlowExecutionStatus, NOT ExitStatus
  5. stepExecution can be null

basics

~20 s

A JobExecutionDecider is a small class whose decide() method returns a status that tells the job which path to take next. You use it when the choice depends on custom logic, not just whether a step passed or failed.

solid answer

~40 s

JobExecutionDecider is an interface with one method, decide(JobExecution, StepExecution), that returns a FlowExecutionStatus. In a job's flow you wire it with .next(decider).on("SOME_STATUS").to(nextStep). The framework calls decide(), reads the returned status name, and matches it against your .on(...) patterns to pick the next transition. You reach for it when a step's ExitStatus alone can't express the branching decision — for example choosing a path based on a row count, the day of week, a value in the JobExecutionContext, or external state. It keeps branching logic out of the steps themselves and makes the flow graph explicit and testable, since a decider is a plain bean you can unit test in isolation.

code

java · 12 lines
java
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;

public class RowCountDecider implements JobExecutionDecider {
    @Override
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        long rows = jobExecution.getExecutionContext().getLong("importedRows", 0);
        return new FlowExecutionStatus(rows > 1000 ? "LARGE" : "SMALL");
    }
}

go deeper

for a junior

Know it's a class with decide() that picks the next step, and that it returns a FlowExecutionStatus wired via .on(...).to(...).

for a middle

Explain FlowExecutionStatus vs ExitStatus and give a concrete branching use-case (counts, calendar, flags).

for a senior

Discuss reading state from JobExecutionContext, the null stepExecution case, and keeping deciders side-effect free for testability.

for a principal

Frame deciders as making the flow graph explicit and restart-safe; weigh them against alternatives like split flows or externalized orchestration.

## The problem it solves A Spring Batch **Job** is a graph of **Steps** connected by transitions. By default you move between steps based on a step's **ExitStatus** — a String like `COMPLETED` or `FAILED` that a step produces when it finishes. You branch with `.on("COMPLETED").to(stepB)`. Sometimes the decision about *where to go next* isn't about whether a step succeeded — it's about business logic. Examples: 'if we imported more than 1000 rows, run the reconciliation step; otherwise skip it', 'on the first of the month, run the billing branch', 'if a feature flag is on, take path A'. A step's ExitStatus can't cleanly carry that, and overloading it forces the step to know about downstream routing. That coupling is what a decider removes. ## The interface `org.springframework.batch.core.job.flow.JobExecutionDecider` has exactly one method: ```java FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution); ``` - **JobExecution** — the running job instance; gives access to the **JobExecutionContext** (a key/value bag that survives between steps) and job parameters. - **StepExecution** — the execution of the step immediately before the decider, or **null** if no step ran before it (e.g. the decider is the very first flow element). This is a common gotcha: guard against null. - The return type is **FlowExecutionStatus**, NOT ExitStatus. It wraps a status name String (e.g. `COMPLETED`, `FAILED`, or a custom string like `"LARGE"`). ## How it's wired You register the decider in the flow and branch on the *name* it returns: ```java jobBuilder .start(loadStep) .next(decider) .on("LARGE").to(reconcileStep) .from(decider).on("SMALL").to(notifyStep) .end() .build(); ``` When the flow reaches the decider, the framework calls `decide(...)`, takes the returned `FlowExecutionStatus.getName()`, and matches it against the `.on(pattern)` clauses (pattern matching supports `*` and `?` wildcards). The matching transition's `.to(...)` target runs next. ## When to use it - Branching on aggregate results (counts, sums) computed in a prior step and stashed in the JobExecutionContext. - Time/calendar-based routing. - External conditions (config flags, another system's state). - Any decision where making the *step* emit a routing ExitStatus would be a hack. ## When NOT to use it If the branch is simply 'did the step succeed or fail', use `.on("COMPLETED")` / `.on("FAILED")` on the step's own ExitStatus — no decider needed. ## Key facts to remember - One method, returns FlowExecutionStatus (not ExitStatus). - It's a plain Spring bean — easy to unit test. - stepExecution can be null. - Deciders should be side-effect-free and read state from the JobExecutionContext / parameters so behavior is deterministic and restart-friendly.

  • Where does the decider read the row count from in your example, and who put it there?
    From the JobExecutionContext, a key/value map on the JobExecution that survives across steps. A prior step (typically in a StepExecutionListener's afterStep, promoted to the job context, or written directly) stored 'importedRows' so the decider can read it.
  • What does the .on(...) string actually match against?
    The name of the FlowExecutionStatus the decider returns (FlowExecutionStatus.getName()), using pattern matching with * and ? wildcards — not the step's ExitStatus.

saying these in an interview costs you the question

  • Saying decide() returns an ExitStatus (it returns FlowExecutionStatus).
  • Claiming a decider replaces the step — it sits between steps and only chooses a path.
  • Assuming stepExecution is always non-null.
  • Putting the routing decision inside the step's business logic instead of a decider.

context