What is a JobExecutionDecider in Spring Batch and why would you use one instead of relying on a step's ExitStatus?
answer
- decide(JobExecution, StepExecution) -> FlowExecutionStatus
- branch on custom logic, not just pass/fail
- .next(decider).on("NAME").to(step)
- returns FlowExecutionStatus, NOT ExitStatus
- stepExecution can be null
basics
~20 sA 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 sJobExecutionDecider 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 linesimport 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
Know it's a class with decide() that picks the next step, and that it returns a FlowExecutionStatus wired via .on(...).to(...).
Explain FlowExecutionStatus vs ExitStatus and give a concrete branching use-case (counts, calendar, flags).
Discuss reading state from JobExecutionContext, the null stepExecution case, and keeping deciders side-effect free for testability.
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.