skip to content

By default is jobLauncher.run() blocking or non-blocking, and how do you make it asynchronous?

level: middleimportance: must knowfreq 68%

answer

  1. default SyncTaskExecutor = caller thread = blocks
  2. async = SimpleAsyncTaskExecutor / ThreadPoolTaskExecutor
  3. async returns STARTING/STARTED, poll for result
  4. launcher executor != step executor
  5. SimpleAsyncTaskExecutor is unbounded

basics

~20 s

By default run() is blocking: TaskExecutorJobLauncher uses a SyncTaskExecutor, so it runs the job in the caller thread and returns only when it finishes. Give the launcher an async TaskExecutor to make run() return immediately.

solid answer

~40 s

The default JobLauncher, TaskExecutorJobLauncher, delegates execution to a TaskExecutor. Out of the box that's a SyncTaskExecutor, which runs the task on the calling thread — so run() blocks until the job completes and the returned JobExecution is already in its terminal state (COMPLETED/FAILED). To launch asynchronously you configure the launcher with an async TaskExecutor such as SimpleAsyncTaskExecutor or a ThreadPoolTaskExecutor. Then run() persists the JobExecution, hands the job to the executor thread, and returns almost immediately with the JobExecution typically in STARTING/STARTED — the caller must poll the JobRepository (or use a listener) to learn the outcome. Async is what you want when triggering from a web request so the HTTP thread isn't held for the whole job. In Boot you override the launcher bean or set its TaskExecutor to switch modes.

code

java · 15 lines
java
// Async launcher so a web request returns immediately
@Bean
JobLauncher asyncJobLauncher(JobRepository repo) throws Exception {
    var executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(4);
    executor.setQueueCapacity(10);
    executor.initialize();

    var launcher = new TaskExecutorJobLauncher();
    launcher.setJobRepository(repo);
    launcher.setTaskExecutor(executor); // <- async; SyncTaskExecutor is the default
    launcher.afterPropertiesSet();
    return launcher;
}

go deeper

for a junior

Know the default is blocking and that an async executor makes run() return early.

for a middle

Name SyncTaskExecutor vs SimpleAsyncTaskExecutor/ThreadPoolTaskExecutor and describe the returned status difference.

for a senior

Explain the web-trigger motivation, poll-for-result pattern, and pool sizing; distinguish launcher executor from step executor.

for a principal

Reason about concurrency limits, backpressure, unbounded-thread risk, and observability of async runs via metadata.

## The mechanism `TaskExecutorJobLauncher` (the default `JobLauncher`; it was called `SimpleJobLauncher` before Spring Batch 5) holds a `org.springframework.core.task.TaskExecutor`. When you call `run(job, params)` it: 1. Validates the parameters and checks the `JobRepository` for prior instances/executions. 2. Creates and **persists** a new `JobExecution` (this is why a `JobRepository` is mandatory). 3. Submits the actual job execution to its `TaskExecutor`. 4. Returns the `JobExecution`. The **behaviour of step 3-4 depends entirely on which TaskExecutor is configured.** ## Default: SyncTaskExecutor (blocking) `SyncTaskExecutor.execute(runnable)` simply calls `runnable.run()` **on the current thread** — no new thread, no queue. So `run()` does not return until the job is fully finished. The `JobExecution` you get back is already in a **terminal** `BatchStatus` (`COMPLETED`, `FAILED`, `STOPPED`). This is the default because it's the safest and most predictable, and it's exactly what you want for CLI-style / on-startup batch runs. ## Async: SimpleAsyncTaskExecutor or ThreadPoolTaskExecutor Inject an asynchronous `TaskExecutor` and `run()` returns as soon as the JobExecution is persisted and handed off. The returned `JobExecution` is typically in `STARTING` or `STARTED`, **not** terminal. The caller cannot read the final status synchronously — it must: - poll via `JobExplorer`/`JobRepository` using the execution id, or - register a `JobExecutionListener` / rely on side effects. ```java @Bean JobLauncher asyncJobLauncher(JobRepository jobRepository) throws Exception { TaskExecutorJobLauncher launcher = new TaskExecutorJobLauncher(); launcher.setJobRepository(jobRepository); launcher.setTaskExecutor(new SimpleAsyncTaskExecutor()); // async launcher.afterPropertiesSet(); return launcher; } ``` - **`SimpleAsyncTaskExecutor`** spawns a **new thread per task** and does not reuse them (you can cap concurrency). Fine for occasional launches; dangerous under high launch rates because it's unbounded by default. - **`ThreadPoolTaskExecutor`** uses a bounded pool + queue — the production-grade choice; sizing the pool caps how many jobs run concurrently. ## Why choose async The classic case: a **REST endpoint** or message listener triggers a long job. With the default sync launcher the HTTP worker thread is held for the whole job (could be minutes/hours) — terrible. An async launcher returns the execution id instantly; the client polls a status endpoint. ## Gotchas - **Don't confuse the launcher's TaskExecutor with a Step's TaskExecutor.** The launcher's executor decides sync vs async *launch*; a `Step`'s `taskExecutor(...)` decides multi-threaded *chunk processing within* a step. Different concerns. - With async, a returned non-terminal status is normal — code that asserts `COMPLETED` right after `run()` will break. - `SimpleAsyncTaskExecutor` is **not** a pool; unbounded thread creation can exhaust the JVM. Prefer `ThreadPoolTaskExecutor` (or set a concurrency limit) in production. - Because the JobExecution is persisted *before* execution, even an async job is fully tracked in the metadata tables and restartable. - Overriding Boot's launcher bean: define your own `JobLauncher` bean and Boot backs off; `JobLauncherApplicationRunner` will then use your (possibly async) launcher.

  • After an async run(), what BatchStatus will the returned JobExecution usually have?
    A non-terminal one — STARTING or STARTED. You must poll the JobRepository/JobExplorer by execution id (or use a listener) for the final COMPLETED/FAILED status.
  • Why is SimpleAsyncTaskExecutor risky in production?
    It creates a new thread per launch and by default is unbounded, so a burst of launches can exhaust threads/memory. A bounded ThreadPoolTaskExecutor caps concurrency.

saying these in an interview costs you the question

  • Claiming run() is non-blocking by default.
  • Confusing the launcher's TaskExecutor with a Step's multi-threaded taskExecutor.
  • Expecting a COMPLETED status immediately after an async run().
  • Thinking SimpleAsyncTaskExecutor is a pooled/bounded executor.

context