skip to content

Explain the difference between noIsolation(), classLoaderIsolation(), and processIsolation() when obtaining a WorkQueue. When would you choose each?

level: seniorimportance: must knowfreq 50%

answer

  1. noIsolation: build JVM, plugin classpath
  2. classLoaderIsolation: isolated classloader, custom classpath
  3. processIsolation: forked JVM, forkOptions heap/args
  4. least isolation that's correct
  5. forked workers reused by spec

basics

~20 s

noIsolation runs in the build JVM with the plugin's classpath. classLoaderIsolation runs in a separate classloader so deps don't leak. processIsolation runs in a forked JVM with its own classpath, heap, and JVM args. More isolation costs more startup overhead.

solid answer

~50 s

`WorkerExecutor` gives three queue flavors. **`noIsolation()`** runs actions in the Gradle build process using the plugin's own classloader — fastest, but the action shares Gradle's classpath and can see/clash with build internals. **`classLoaderIsolation()`** still runs in the build JVM but in an **isolated classloader** you can configure with a custom classpath, so your tool's dependencies (and any conflicting versions) are separated from Gradle and other tasks — good when you need a clean classpath but not a separate JVM. **`processIsolation()`** forks a **separate worker JVM** with its own classpath, heap, system properties, and JVM args (configured via `forkOptions`); use it when the work needs different JVM settings, large/risky memory, native crashes contained, or a tool that demands process-level separation (e.g. annotation processors, javadoc, external compilers). The trade-off is startup cost: noIsolation ≈ free, classLoaderIsolation cheap, processIsolation expensive (JVM fork, though Gradle reuses worker daemons). Choose the least isolation that is correct.

code

kotlin · 9 lines
kotlin
val procQueue = workerExecutor.processIsolation { spec ->
    spec.classpath.from(toolClasspath)
    spec.forkOptions { fork ->
        fork.maxHeapSize = "512m"
        fork.jvmArgs("-XX:+UseParallelGC")
        fork.systemProperty("file.encoding", "UTF-8")
    }
}
procQueue.submit(LegacyToolAction::class.java) { it.input.set(f) }

go deeper

for a junior

Name the three modes and that more isolation means more overhead.

for a middle

Describe what each isolates (classpath vs JVM) and a concrete use case for each.

for a senior

Articulate the cost/benefit trade-off, custom classpath/forkOptions wiring, and 'least isolation that is correct'.

for a principal

Guide plugin teams on isolation strategy across a portfolio, worker reuse implications, and fault-containment requirements for external tooling.

## Three queues, three isolation levels `WorkerExecutor` exposes three factory methods, each returning a `WorkQueue`: ### 1. `noIsolation()` - Runs the `WorkAction` **in the Gradle build process**, on the build's own classloader. - No classpath separation: your action sees Gradle's classes and whatever the plugin classpath has. - **Cheapest** — no fork, no classloader setup. Still parallel across worker threads. - Use when: the work is pure JVM code with no dependency conflicts and no special JVM needs (e.g. transforming files using libraries already on the plugin classpath). ### 2. `classLoaderIsolation()` - Runs **in the build JVM** but inside a **new, isolated classloader**. - You can supply a custom classpath via `spec.classpath.from(...)`, so the action's libraries are separated from Gradle's and from other tasks. Prevents version clashes (e.g. your tool needs Guava 32 while Gradle ships another). - Slightly more expensive than noIsolation (classloader construction), far cheaper than a fork. - Use when: you need classpath cleanliness/conflict avoidance but the work is safe to run in-process. ### 3. `processIsolation()` - **Forks a separate worker JVM**. The action runs there with its own classpath, heap, system properties, and JVM arguments configured through `spec.forkOptions { ... }` (and `spec.classpath`). - Strongest isolation: a native crash or `System.exit` doesn't take down the build; you can give the worker `-Xmx`, encoding, or module flags independent of the build JVM. - **Most expensive** (JVM startup), but Gradle keeps a pool of reusable worker processes (worker daemons) keyed by their fork settings, so repeated submissions with the same options reuse a process. - Use when: running external/legacy tools that demand process separation, need specific JVM args/heap, or must be sandboxed from the build JVM. ```kotlin // classLoaderIsolation with a custom classpath val clQueue = workerExecutor.classLoaderIsolation { spec -> spec.classpath.from(toolClasspath) } // processIsolation with fork options val procQueue = workerExecutor.processIsolation { spec -> spec.classpath.from(toolClasspath) spec.forkOptions { fork -> fork.maxHeapSize = "512m" fork.systemProperty("file.encoding", "UTF-8") } } ``` ## Choosing Rule of thumb: **use the least isolation that is still correct.** Start with `noIsolation`; move to `classLoaderIsolation` if you hit dependency conflicts or need a controlled classpath; move to `processIsolation` only when you need a distinct JVM (custom args/heap, fault containment, tools requiring it). Each step up adds latency. ## Interaction with the worker pool All three respect `--max-workers`: the total number of concurrently running units across the build (in-process threads + forked processes) stays within budget. Forked workers are reused across tasks when their fork/classpath spec matches, amortizing JVM startup.

  • Forking a JVM per unit of work sounds slow — how does Gradle keep processIsolation viable?
    Gradle maintains a pool of reusable worker processes keyed by their fork/classpath spec. Units with matching options reuse an already-started worker JVM, so you pay startup roughly once, not per submission.
  • If a worker action calls System.exit or crashes the JVM, what happens under each isolation mode?
    Under noIsolation/classLoaderIsolation it runs in the build JVM, so a crash or System.exit can take down the build. Under processIsolation it only kills the forked worker, which Gradle reports as a failed unit while the build JVM survives.

noIsolation is working at your own desk; classLoaderIsolation is a soundproof booth in the same office (your own tools, shared building); processIsolation is renting a separate building with its own power and locks.

saying these in an interview costs you the question

  • Claiming classLoaderIsolation forks a new JVM — it stays in the build process, only the classloader is isolated.
  • Defaulting everything to processIsolation 'to be safe' and ignoring the startup cost.
  • Saying noIsolation can't run in parallel — it can; it just lacks classpath/process separation.

context