skip to content

JobExecutionDecider

A JobExecutionDecider computes the next path in code when the exit status alone cannot express the rule. The answer whenever the branch depends on data rather than on whether a step succeeded.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

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

open as a page

Show how you wire a JobExecutionDecider into a job's flow with .next(decider).on(...).to(...), including branching to multiple paths.

level: middleimportance: must knowfreq 50%

basics

~10 s

After a step, call .next(decider), then chain .on("NAME").to(step) for each possible status. Use .from(decider).on("OTHER").to(otherStep) to add more branches, and finish with .end().build().

open as a page

What is the difference between FlowExecutionStatus (returned by a decider) and ExitStatus (produced by a step), and how do they relate to BatchStatus?

level: middleimportance: should knowfreq 45%

basics

~20 s

A step produces an ExitStatus (a String outcome) that flow transitions match on. A decider returns a FlowExecutionStatus, a similar named status but produced by code instead of a step. Both are just names used to pick the next path; BatchStatus is the separate framework-level state (STARTED, COMPLETED, FAILED).

open as a page

How does a decider access data to make its decision, and what are the gotchas with the StepExecution argument and restart behavior?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The decider reads from the JobExecution (job parameters, JobExecutionContext) and the preceding StepExecution (counts, its own step context). The StepExecution can be null if no step ran before the decider, so guard for it. Deciders should be deterministic so restarts route the same way.

open as a page

When would you choose a JobExecutionDecider versus alternatives (custom step ExitStatus, split/parallel flows, or externalizing orchestration), and what trade-offs guide that decision?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Use a decider when routing depends on logic a step's pass/fail can't express and you want the branch explicit in the job graph. Use step ExitStatus for simple success/failure branching, split flows for parallel independent paths, and external orchestration when the workflow spans many jobs or systems.

open as a page