Show how you wire a JobExecutionDecider into a job's flow with .next(decider).on(...).to(...), including branching to multiple paths.
answer
- .start(step).next(decider).on("X").to(...)
- .from(decider) re-anchors each extra branch
- terminal ops: end() fail() stop() stopAndRestart()
- always add .on("*") catch-all
- decider can be .start() with null stepExecution
basics
~10 sAfter 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().
solid answer
~40 sIn Spring Batch 5 you build the job from a JobBuilder. Start a step, then .next(decider) hands the flow to the decider. Each branch is an .on(pattern).to(target) pair matched against the FlowExecutionStatus name the decider returns. To declare more than one branch from the same decider you re-anchor with .from(decider).on(pattern).to(target). Targets can be steps or nested flows, and a branch can end the job with .end() or fail it with .fail(). You close the flow with .end() and call .build(). The decider itself is a @Bean implementing JobExecutionDecider. Order matters only for specificity: the most specific pattern wins, and you typically add a .on("*") catch-all so an unexpected status doesn't blow up the job with 'next state not found'.
code
java · 27 linesimport org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ReportJobConfig {
@Bean
Job reportJob(JobRepository repo,
Step loadStep, Step bigReportStep, Step smallReportStep,
JobExecutionDecider sizeDecider) {
return new JobBuilder("reportJob", repo)
.start(loadStep)
.next(sizeDecider)
.on("LARGE").to(bigReportStep)
.from(sizeDecider)
.on("SMALL").to(smallReportStep)
.from(sizeDecider)
.on("*").fail()
.end()
.build();
}
}go deeper
Reproduce the basic .start().next(decider).on().to().end().build() chain.
Add multiple branches with .from(decider), a catch-all, and correct terminal ops.
Explain specificity-based matching, decider-as-first-element null handling, and converging branches.
Judge when the flow graph is getting too complex and should be refactored into nested flows, split parallelism, or externalized orchestration.
## The builder chain Since Spring Batch 5 (Spring Boot 3), you no longer extend deprecated configurer classes; you inject a `JobRepository` and `PlatformTransactionManager` and use `JobBuilder` / `StepBuilder`. The flow DSL for deciders reads like a state machine: ```java @Bean Job reportJob(JobRepository repo, Step loadStep, Step bigStep, Step smallStep, JobExecutionDecider sizeDecider) { return new JobBuilder("reportJob", repo) .start(loadStep) // run a step first .next(sizeDecider) // hand control to the decider .on("LARGE").to(bigStep) // branch 1: status name == LARGE .from(sizeDecider) // re-anchor to add another branch .on("SMALL").to(smallStep) // branch 2 .from(sizeDecider) .on("*").fail() // catch-all safety net .end() // close the flow .build(); } ``` ### Element by element - **.start(loadStep)** — first step; returns a `SimpleJobBuilder`. - **.next(sizeDecider)** — inserts the decider; returns a `JobFlowBuilder` exposing `.on(...)`. - **.on("LARGE")** — pattern matched against the decider's `FlowExecutionStatus.getName()`. Wildcards `*` and `?` supported. - **.to(bigStep)** — transition target when the pattern matches. A target may itself be a step, a decider, or a nested flow. - **.from(sizeDecider)** — you must re-anchor to the decider (or step) to declare each additional outgoing transition; you can't chain multiple `.on()` off one `.to()`. - **.on("*").fail()** — catch-all; terminal operations include `.end()` (complete), `.fail()` (mark FAILED), `.stop()` (stop, restartable), `.stopAndRestart(step)`. - **.end().build()** — finalize the FlowJobBuilder into a Job. ## A decider can be the first element ```java new JobBuilder("job", repo) .start(decider) // no prior step .on("A").to(stepA) .from(decider).on("B").to(stepB) .end().build(); ``` Here `stepExecution` passed to `decide` is **null** because no step ran before it — handle that. ## Multiple branches / fan pattern Each `.from(decider).on(x).to(y)` adds an edge. You can route to different steps, converge them later (`.next(...)` after `.to(step)` continues that branch), or terminate each branch independently. This is a directed graph, so keep it readable — deeply nested flows are a maintenance smell. ## Common mistakes 1. **Forgetting .from(decider) for the second branch** — chaining a second `.on()` directly won't attach it to the decider. 2. **No catch-all** — an unmatched status throws; add `.on("*")`. 3. **Using .next(step) instead of a branch** after a decider when you actually meant conditional routing. 4. **Assuming order = priority** — matching is by specificity (most specific wins), not declaration order; `*` is least specific. 5. **Registering the decider inline vs as a bean** — either works, but a `@Bean` is testable and reusable. ## Kotlin note Same API from Kotlin; the fluent chain is identical. No special DSL needed.
- Why do you need .from(decider) for the second branch instead of chaining another .on()?Because .to(step) moves the builder's cursor to that step. To add another outgoing edge from the decider you must re-anchor with .from(decider); otherwise the new .on() would attach to the wrong node (or not compile).
- What terminal operations can end a branch, and how do they differ?.end() completes the flow (BatchStatus COMPLETED), .fail() marks it FAILED, .stop() stops it in a restartable state, and .stopAndRestart(step) stops but designates where a restart resumes.
saying these in an interview costs you the question
- Chaining two .on() clauses off one decider without .from(decider).
- Believing branch declaration order determines match priority (it's specificity).
- Omitting a catch-all and being surprised by 'next state not found'.
- Thinking .next(decider) runs the decider as a step with its own StepExecution row.