How do you make a Spring Batch job re-runnable on demand (e.g. every night) without hitting JobInstanceAlreadyCompleteException?
answer
- JobParametersIncrementer.getNext(prev)
- RunIdIncrementer bumps run.id
- attach via .incrementer(...)
- -next / getNextJobParameters applies it
- business date param keeps the guard
basics
~10 sAdd a JobParametersIncrementer, such as RunIdIncrementer, so each launch gets a new identifying parameter (like run.id). That makes every launch a new JobInstance, so the completed-instance guard never triggers.
solid answer
~40 sBecause a completed JobInstance can't re-run, a repeatable job needs a fresh identifying parameter each time. Register a `JobParametersIncrementer` on the job via `.incrementer(...)`. The built-in `RunIdIncrementer` reads the previous `run.id`, increments it, and adds it as a new identifying parameter, so every launch resolves to a new JobInstance. The incrementer is applied when you launch with the 'next' semantics — e.g. `CommandLineJobRunner -next`, or Spring Boot's `JobLauncherApplicationRunner` when configured, or `JobParametersBuilder.getNextJobParameters(job)`. For real jobs, a business-meaningful incrementing value (a run date or sequence) is preferable to `run.id` because it keeps the metadata auditable. Note that using an incrementer means you give up automatic restart-on-failure, since each launch is a distinct instance.
code
java · 25 lines@Bean
Job dailyEtlJob(JobRepository repo, Step loadStep) {
return new JobBuilder("dailyEtlJob", repo)
.incrementer(new RunIdIncrementer()) // adds/increments run.id
.start(loadStep)
.build();
}
// Programmatic 'next' launch — applies the incrementer to the previous params:
JobParameters next = new JobParametersBuilder(jobExplorer)
.getNextJobParameters(dailyEtlJob)
.toJobParameters();
jobLauncher.run(dailyEtlJob, next); // run.id = 1, then 2, then 3 ...
// Custom business-key incrementer keeps the completed-instance guard useful:
class BusinessDateIncrementer implements JobParametersIncrementer {
@Override public JobParameters getNext(JobParameters p) {
LocalDate prev = p.getString("businessDate") == null
? LocalDate.now().minusDays(1)
: LocalDate.parse(p.getString("businessDate"));
return new JobParametersBuilder(p)
.addString("businessDate", prev.plusDays(1).toString())
.toJobParameters();
}
}go deeper
Knows you need to change a parameter to run again; may just pass a timestamp.
Registers RunIdIncrementer and knows it must be triggered by a next-style launch.
Weighs run.id vs a business-key incrementer and understands the restart trade-off.
Designs the run-key policy (advance vs restart-same-date) around correctness and auditability.
**The problem.** Spring Batch's safety guard throws `JobInstanceAlreadyCompleteException` when you re-launch with identical identifying parameters that already COMPLETED. A daily/hourly job would hit this immediately if its parameters never changed. The solution is to make each intended run a **new JobInstance** by varying an identifying parameter. **JobParametersIncrementer.** This is a single-method interface: ```java public interface JobParametersIncrementer { JobParameters getNext(JobParameters parameters); } ``` Given the previous run's parameters, it returns the parameters for the next run. You attach it to a job with the builder's `.incrementer(...)`. **RunIdIncrementer.** The framework's built-in implementation adds/updates an identifying long parameter named `run.id`: it reads the previous `run.id` (default start 0), adds 1, and sets it. So run.id goes 1, 2, 3… — each value a distinct instance. **How the incrementer actually gets invoked.** The incrementer is *not* applied automatically on a plain `jobLauncher.run(job, params)`. It is applied by 'next'-style launch paths: - `CommandLineJobRunner` with the `-next` option. - `JobLauncherApplicationRunner` in Spring Boot (it will use the incrementer when launching on startup). - Programmatically via `new JobParametersBuilder(previousParams, jobExplorer).getNextJobParameters(job).toJobParameters()`, or the `getNextJobParameters(job)` helper which loads the last instance's parameters and applies the incrementer. **Business-key alternative.** Instead of an opaque `run.id`, many teams pass an identifying `runDate`/`businessDate` parameter (today's date, the batch window). This both guarantees uniqueness per day *and* preserves the idempotency guard: accidentally launching twice for the same business date is correctly rejected. You can write a custom incrementer (e.g. `DailyDateIncrementer`) that advances the date. **Trade-off with restart.** An incrementer and automatic failure-restart are somewhat at odds. If run.id=5 fails and you launch again with -next, you get run.id=6 — a *new* instance from scratch, not a resume of run.id=5. To restart the failed instance you must re-launch with the *same* parameters (run.id=5). Teams choose their policy: business jobs often prefer 'restart the same business date' over 'always move forward'. **Gotchas.** - Forgetting the incrementer and re-launching a plain job → JobInstanceAlreadyCompleteException on the second run. - Using `System.currentTimeMillis()` as an identifying param 'works' but destroys the guard and floods the metadata — an anti-pattern versus a real business key. - The incrementer only runs on next-style launches; a direct `run()` with hardcoded params ignores it.
- Does a plain `jobLauncher.run(job, params)` automatically apply the incrementer?No. The incrementer is only applied by 'next'-style launches (`-next`, `getNextJobParameters(job)`, `JobLauncherApplicationRunner`). A direct `run()` with hardcoded parameters ignores the incrementer, so if those params already completed you still get the exception.
- Why might a business-date parameter be better than RunIdIncrementer's run.id?A business date is auditable and self-documenting, and it preserves the idempotency guard: relaunching for the same date is correctly rejected as a duplicate. An opaque run.id always advances, so it can mask an accidental double-run for the same logical period.
saying these in an interview costs you the question
- Claiming the incrementer runs on every jobLauncher.run call
- Using System.currentTimeMillis() as the uniqueness strategy
- Thinking an incrementer lets you restart a failed instance (it creates a new one instead)