skip to content

A developer sees 'Scope "step" is not active for the current thread' and, separately, notices a @StepScope reader captured a stale job parameter across launches. Diagnose both and explain the underlying threading/lifecycle model.

level: principalimportance: should knowfreq 24%

answer

  1. StepSynchronizationManager ThreadLocal binds StepExecution
  2. 'Not active' = no context bound (PostConstruct/other thread/before launch)
  3. Stale param = bean isn't really @StepScope (singleton captured once)
  4. One @StepScope instance per StepExecution, shared across worker threads
  5. Prefer partitioning; SynchronizedItemStreamReader for MT steps

basics

~20 s

The 'scope not active' error means the step-scoped bean was touched when no StepExecution was bound to the thread — e.g. in @PostConstruct, from another thread, or before launching. Stale parameters usually mean the bean isn't actually @StepScope (a singleton captured the value once), or state leaked because the instance was shared.

solid answer

~50 s

Both symptoms come from the step-scope binding model. Step scope resolves the real bean from a StepContext held in a ThreadLocal by StepSynchronizationManager, bound only while a StepExecution runs on that thread. 'Scope step is not active' means the scoped bean (or its proxy) was dereferenced with no context bound: from @PostConstruct, an eager singleton dependency, a scheduler or async thread, or a test invoking it before job launch. Fix by ensuring access happens inside the step, or don't make the caller a startup singleton. The stale-parameter symptom almost always means the bean is not truly step-scoped — a missing @StepScope (or a plain singleton reading the SpEL once at startup), so the first-evaluated value sticks. It can also arise if a step-scoped reader holds mutable state and is shared across a multi-threaded step's worker threads without being thread-safe. The cure is correct scoping plus per-run instantiation, and thread-safe or partitioned state.

code

java · 23 lines
java
// BUG A: 'Scope step is not active' — singleton calls scoped bean at startup.
@Component
public class Warmup {
    Warmup(ItemReader<Row> reader) { reader.read(); } // proxy invoked, no step bound -> throws
}

// BUG B: stale parameter — missing @StepScope, SpEL evaluated once at startup.
@Bean // <-- no @StepScope: singleton captures the first launch's value forever
public FlatFileItemReader<Row> reader(
        @Value("#{jobParameters['inputFile']}") String path) { ... }

// FIX B + thread-safety for a multi-threaded step:
@Bean
@StepScope // new instance per StepExecution -> fresh jobParameters each launch
public SynchronizedItemStreamReader<Row> safeReader(
        @Value("#{jobParameters['inputFile']}") String path) {
    var delegate = new FlatFileItemReaderBuilder<Row>()
        .name("rowReader").resource(new FileSystemResource(path))
        .lineMapper(rowLineMapper()).build();
    var sync = new SynchronizedItemStreamReader<Row>();
    sync.setDelegate(delegate); // safe for worker threads sharing one instance
    return sync;
}

go deeper

for a junior

Recognize that touching a step-scoped bean outside a running step fails, and that missing @StepScope causes stale values.

for a middle

Tie both errors to the ThreadLocal step-context binding and know to add @StepScope.

for a senior

Explain multi-threaded sharing (one instance per StepExecution) and thread-safety mitigations.

for a principal

Reason about the full binding lifecycle, partitioning vs multi-threaded trade-offs, and alternatives that read parameters straight from the execution objects.

## The binding model (the key to both bugs) Step scope is a **thread-bound** scope. `StepSynchronizationManager` keeps a **`ThreadLocal`** stack of `StepContext`. Spring Batch's `AbstractStep.execute()` **registers** the current `StepExecution` before running the step body and **unregisters** it afterward (in a `finally`). The scoped proxy resolves the real bean **only** while a context is registered on the calling thread. Everything below follows from this. ## Symptom 1: `Scope 'step' is not active for the current thread` Thrown when the proxy tries to resolve its target but no `StepContext` is bound. Common triggers: 1. **`@PostConstruct` / constructor / eager singleton dependency** — a startup-time singleton that *calls a method* on the injected step-scoped proxy (merely holding the proxy is fine; invoking it at startup is not). 2. **Wrong thread** — a `@Scheduled`, `@Async`, or a `TaskExecutor` thread that isn't a batch worker for a currently-registered step. The `ThreadLocal` is not populated there. 3. **Before/after the step** — touching the bean before launching the job or after the step completes (e.g. in a test, or a `JobExecutionListener.afterJob`). 4. **Multi-threaded step propagation edge** — if you spawn your own threads inside a step, they don't inherit the `ThreadLocal` unless the context is propagated. **Fixes:** access the bean only during step execution; avoid making the accessor an eager singleton; if you need late-bound data outside a step, read `JobParameters`/`ExecutionContext` from the passed-in `StepExecution`/`ChunkContext` directly rather than through a step-scoped bean; for custom threads, use Spring Batch's own multi-threaded step or task executor which manage registration. ## Symptom 2: stale job parameter across launches Expected behavior: each launch is a new `StepExecution`, so a `@StepScope` bean is **re-created** and re-evaluates its SpEL against the **new** `jobParameters`. If a value looks stale: 1. **Missing `@StepScope`** (the classic): the bean is a **singleton**; its `@Value("#{jobParameters['x']}")` was evaluated **once at startup** (and may have even 'worked' the first launch by luck if the context was still resolving), then never again. Every subsequent launch sees the first value. Add `@StepScope`. 2. **Scope on the wrong bean** — e.g. the reader is `@StepScope` but it delegates to a **singleton** helper that captured the parameter; the helper must be scoped too. 3. **Static/`@Bean` returning a cached instance** — returning a pre-built singleton from a `@StepScope` method (e.g. a field) defeats per-run creation. 4. **Shared mutable state under a multi-threaded step** — one `@StepScope` instance is shared across the worker threads of a *single* `StepExecution`; if it caches per-item or per-thread state in fields, threads clobber each other, which can look like 'stale'/wrong values. Make it thread-safe or use **partitioning** (each partition = own `StepExecution` = own instance). ## Threading nuance for @StepScope in multi-threaded steps `@StepScope` gives **one instance per `StepExecution`**, not per thread. A `taskExecutor`-backed chunk step runs multiple worker threads **within the same `StepExecution`**, all sharing that **one** scoped instance. Many `ItemReader`s are **not thread-safe** (e.g. cursor-based). Guidance: use `SynchronizedItemStreamReader`, a thread-safe reader, or prefer **partitioning** where each partition is a separate `StepExecution` with its own step-scoped instance and its own `stepExecutionContext` — which also restores clean late binding per partition. ## Design guidance - Keep step-scoped beans **stateless** or explicitly thread-safe. - Don't reach a step-scoped bean from startup singletons, schedulers, or async threads. - Prefer partitioning over multi-threaded steps when readers hold state. - If you only need late-bound data (not a scoped bean), pull it from the `StepExecution`/`ChunkContext` handed to your component.

  • In a multi-threaded chunk step (taskExecutor), how many instances of a @StepScope reader exist, and what's the risk?
    Exactly one per StepExecution — shared across all worker threads. The risk is that non-thread-safe stateful readers (e.g. cursor readers) get corrupted by concurrent access. Wrap in SynchronizedItemStreamReader or switch to partitioning where each partition gets its own instance.
  • How can you access late-bound job parameters without using a @StepScope bean at all?
    Read them from the execution objects Spring Batch hands you: e.g. chunkContext.getStepContext().getJobParameters(), or inject StepExecution via a listener, or use @BeforeStep to capture the StepExecution. This avoids scope-activation issues when the code runs outside step-bound threads.
  • Why might partitioning be preferable to a multi-threaded step for a stateful reader?
    Each partition is a distinct StepExecution, so each gets its own @StepScope reader instance and its own stepExecutionContext. That eliminates shared-mutable-state races and gives clean per-partition late binding, instead of one shared reader accessed by many threads.

saying these in an interview costs you the question

  • Claiming a multi-threaded step gives one @StepScope instance per thread
  • Blaming the 'not active' error on Spring Batch bugs rather than out-of-step access
  • Assuming @StepScope alone makes a reader thread-safe
  • Not recognizing a missing @StepScope as the cause of stale parameters

context