Why does the JobRepository use a SERIALIZABLE isolation level when creating a JobExecution, and when would you change it?
answer
- createJobExecution = check-then-act
- SERIALIZABLE default prevents duplicate JobInstance race
- isolationLevelForCreate on @EnableBatchProcessing / JobRepositoryFactoryBean
- Applies only to metadata create, not chunk tx
- Lower it → add unique constraint / single launcher
basics
~20 sWhen starting a job, the repository first checks whether an instance/execution already exists, then inserts one. It uses SERIALIZABLE isolation so two launchers can't both create the same JobInstance at once. You lower it only if your DB struggles with SERIALIZABLE.
solid answer
~40 sCreating a `JobExecution` is a check-then-act: the `JobRepository` looks up whether a `JobInstance` for this name+parameters already exists (and whether it's already running/completed), then inserts the new instance/execution rows. If two processes launch the same job concurrently, a weak isolation level allows a race where both pass the check and both insert — producing duplicate instances or two simultaneous executions. To prevent that, the create path runs in its own transaction at `ISOLATION_SERIALIZABLE` by default (configured on `JobRepositoryFactoryBean` / via `@EnableBatchProcessing(isolationLevelForCreate=...)`). You might lower it to `ISOLATION_REPEATABLE_READ` or `READ_COMMITTED` when the database doesn't support serializable well or it causes contention — but then you should guard duplicates another way (e.g. a unique constraint, single-launcher). Only the *create* step uses this level; normal chunk transactions use the default.
code
java · 16 lines// Tune the create-path isolation via @EnableBatchProcessing (Spring Batch 5)
@Configuration
@EnableBatchProcessing(isolationLevelForCreate = "ISOLATION_READ_COMMITTED")
class BatchInfra { }
// Or when building the repository manually:
@Bean
JobRepository jobRepository(DataSource ds, PlatformTransactionManager tm) throws Exception {
JobRepositoryFactoryBean f = new JobRepositoryFactoryBean();
f.setDataSource(ds);
f.setTransactionManager(tm);
// default is ISOLATION_SERIALIZABLE; lower only with another duplicate guard
f.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ");
f.afterPropertiesSet();
return f.getObject();
}go deeper
Know that starting a job first checks for an existing instance before inserting.
Explain the check-then-act race and that SERIALIZABLE is the default guard.
Configure isolationLevelForCreate and reason about when/how to lower it safely with compensating guards.
Design multi-node launch concurrency: DB isolation vs unique constraints vs external distributed locks, and their failure modes.
## The problem: a check-then-act race Launching a job is not a single insert. Internally `JobRepository.createJobExecution(jobName, jobParameters)` does roughly: 1. Compute the instance identity (name + hash of identifying parameters). 2. **Check** whether a `JobInstance` already exists. If it does and the last execution completed successfully, throw `JobInstanceAlreadyCompleteException`; if one is currently running, throw `JobExecutionAlreadyRunningException`. 3. **Act**: insert the `JobInstance` (if new) and a fresh `JobExecution`. Steps 2 and 3 are a classic **check-then-act** across rows. Under a weak transaction isolation level, two concurrent launchers (two schedulers, two pods, a retry storm) can both execute step 2, both see "nothing exists", and both proceed to step 3 — creating **duplicate JobInstances** or launching the **same instance twice**. This corrupts restart semantics and can double-process data. ## The fix: serializable isolation on create Spring Batch runs the create path in a **dedicated transaction** with a configurable isolation level, defaulting to `ISOLATION_SERIALIZABLE`. Serializable makes the concurrent transactions behave as if they ran one after another, so the second launcher either blocks until the first commits (and then sees the row) or fails with a serialization error — either way, no duplicate slips through. On most databases this is enforced via locking or predicate/range locks that prevent the phantom insert. This is why you sometimes see the constant string form: `"ISOLATION_SERIALIZABLE"` (the value of `TransactionDefinition.ISOLATION_SERIALIZABLE`). ## How to configure it - **Spring Batch 5:** `@EnableBatchProcessing(isolationLevelForCreate = "ISOLATION_REPEATABLE_READ")`. - **Manual bean:** `JobRepositoryFactoryBean.setIsolationLevelForCreate("ISOLATION_READ_COMMITTED")` (or `setIsolationLevelForCreateEnum(...)`). Important: this level applies **only** to the metadata create step, not to your business chunk transactions, which use the transaction manager's default. ## When to lower it - **Databases that don't handle SERIALIZABLE well** or where it causes excessive locking/deadlocks (e.g. some Oracle/SQL Server workloads, or where serializable is emulated expensively). - **Connection pools / DBs that reject the level** — for example, some setups need `ISOLATION_READ_COMMITTED`. - When you lower it, **compensate**: rely on a unique constraint on the instance (name+key), a single active launcher, external locking (e.g. ShedLock), or a message-driven single-consumer trigger, so duplicate creation is still impossible. ## Gotchas - Lowering to `READ_UNCOMMITTED`/`READ_COMMITTED` without another guard reintroduces the duplicate-instance race under concurrent launches. - The level is a **string constant** matching `TransactionDefinition` fields; a typo silently falls back or errors depending on version. - This is unrelated to the isolation of your item read/write transactions — don't conflate the two.
- You lower isolationLevelForCreate to READ_COMMITTED for performance. What must you add to stay safe?Another duplicate-prevention guard: a unique constraint on the JobInstance (name + job key), a single active launcher, or an external lock (e.g. ShedLock), so concurrent launches of the same instance still cannot both succeed.
- Does isolationLevelForCreate affect the transactions around item reading/writing?No. It applies only to the metadata create step. Chunk read/process/write transactions use the PlatformTransactionManager's default isolation.
saying these in an interview costs you the question
- Claiming SERIALIZABLE governs the item-processing chunk transactions
- Saying isolation doesn't matter because 'the DB handles it'
- Lowering the level without adding any other duplicate-prevention mechanism
- Thinking the level prevents restarts rather than concurrent duplicate creation