skip to content

How do you make a Spring Batch job re-runnable on demand (e.g. every night) without hitting JobInstanceAlreadyCompleteException?

level: middleimportance: must knowfreq 60%

answer

  1. JobParametersIncrementer.getNext(prev)
  2. RunIdIncrementer bumps run.id
  3. attach via .incrementer(...)
  4. -next / getNextJobParameters applies it
  5. business date param keeps the guard

basics

~10 s

Add 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 s

Because 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
java
@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

for a junior

Knows you need to change a parameter to run again; may just pass a timestamp.

for a middle

Registers RunIdIncrementer and knows it must be triggered by a next-style launch.

for a senior

Weighs run.id vs a business-key incrementer and understands the restart trade-off.

for a principal

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)

context