By default is jobLauncher.run() blocking or non-blocking, and how do you make it asynchronous?
answer
- default SyncTaskExecutor = caller thread = blocks
- async = SimpleAsyncTaskExecutor / ThreadPoolTaskExecutor
- async returns STARTING/STARTED, poll for result
- launcher executor != step executor
- SimpleAsyncTaskExecutor is unbounded
basics
~20 sBy 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 sThe 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// 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
Know the default is blocking and that an async executor makes run() return early.
Name SyncTaskExecutor vs SimpleAsyncTaskExecutor/ThreadPoolTaskExecutor and describe the returned status difference.
Explain the web-trigger motivation, poll-for-result pattern, and pool sizing; distinguish launcher executor from step executor.
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.