skip to content

JUnit 5 lets you choose a parallel execution strategy of dynamic, fixed, or custom. What does each one mean, and how do you control the resulting degree of parallelism?

level: middleimportance: should knowfreq 32%

answer

  1. strategy = dynamic | fixed | custom
  2. dynamic: cores × factor (default 1.0)
  3. fixed.parallelism is mandatory
  4. custom.class implements ParallelExecutionConfigurationStrategy
  5. parallelism is a target, not a hard cap

basics

~10 s

junit.jupiter.execution.parallel.config.strategy picks how the thread pool is sized. dynamic (the default) uses available processors multiplied by ...config.dynamic.factor (default 1.0). fixed uses the exact number in ...config.fixed.parallelism. custom names your own ParallelExecutionConfigurationStrategy class in ...config.custom.class.

solid answer

~40 s

The strategy decides the desired parallelism of the ForkJoinPool that runs the tests. - **dynamic** (default): parallelism = `Runtime.availableProcessors()` × `junit.jupiter.execution.parallel.config.dynamic.factor`, which defaults to 1.0. Setting the factor to 0.5 halves it; 2.0 doubles it, which is what you want for I/O-bound tests that spend their time waiting. - **fixed**: you state the number yourself in `junit.jupiter.execution.parallel.config.fixed.parallelism`. Preferred on CI, where the container may see the host's core count rather than its own CPU quota and `dynamic` would over-subscribe. - **custom**: `junit.jupiter.execution.parallel.config.custom.class` names a class implementing `ParallelExecutionConfigurationStrategy`, letting you compute parallelism from anything — an environment variable, a machine class, a licence limit. Since 5.10 both built-in strategies also expose a maximum pool size and a `saturate` flag, because tests that block let the pool grow beyond the configured parallelism to avoid starvation.

code

properties · 5 lines
properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4

go deeper

for a junior

Name the three strategies and say that dynamic multiplies the core count by a factor.

for a middle

Give the exact parameter names, the default factor of 1.0, and the CPU-bound versus I/O-bound reasoning for choosing a value.

for a senior

Argue for fixed on CI where the JVM's core count is unreliable, and explain saturation and why parallelism is a target rather than a cap.

for a principal

Treat it as capacity planning: measure wall-clock against failure rate, decide the policy per suite class, and know when the real bottleneck is an external system rather than threads.

## What a strategy configures When parallel execution is enabled, the Jupiter engine runs the test tree on a `ForkJoinPool`. A `ParallelExecutionConfiguration` supplies that pool's numbers — chiefly the **parallelism** (the target number of actively running threads), plus a minimum runnable count, a maximum pool size, a keep-alive time, and whether the pool may saturate. The configuration parameter ``` junit.jupiter.execution.parallel.config.strategy = dynamic | fixed | custom ``` selects who produces that configuration. Note this is orthogonal to the execution *mode*: modes decide **which nodes may overlap**, the strategy decides **how many threads are available** to overlap them. Both are needed; neither substitutes for the other. ## dynamic The default. Parallelism is computed from the machine: ``` parallelism = ceil(Runtime.getRuntime().availableProcessors() × factor) ``` where the factor comes from `junit.jupiter.execution.parallel.config.dynamic.factor` and defaults to `1.0`. So on an 8-core machine you get 8 by default. The factor is the tuning dial. For **CPU-bound** unit tests, a factor around 1.0 is right — more threads than cores just adds context switching. For **I/O-bound** tests (HTTP calls, database round-trips, `Thread.sleep`-style waits) each thread is idle most of the time, so factors of 2–4 often cut wall-clock time substantially. Values below 1.0 are the polite choice on a developer laptop where the IDE and the browser also want CPU. The hazard of `dynamic` is that `availableProcessors()` reports what the JVM believes it has. In a container with a fractional or capped CPU quota, modern JVMs respect cgroup limits reasonably well, but on shared CI executors you can still end up sizing to the host's 64 cores while your job is allowed a fraction of that — producing heavy contention and *slower* runs than sequential. ## fixed ``` junit.jupiter.execution.parallel.config.strategy = fixed junit.jupiter.execution.parallel.config.fixed.parallelism = 4 ``` The `fixed.parallelism` parameter is mandatory for this strategy — omit it and configuration fails. The value is used verbatim regardless of the machine. This is the strategy to reach for when reproducibility matters: identical thread counts on every developer machine and every CI agent make timing-sensitive failures reproducible and stop a beefy workstation from hiding a race that a two-core CI box exposes (or vice versa). It is also the honest answer when your tests contend on one external resource — a single database — where more than N concurrent tests only queues on the far side. ## custom ``` junit.jupiter.execution.parallel.config.strategy = custom junit.jupiter.execution.parallel.config.custom.class = com.example.CiAwareStrategy ``` The named class implements `ParallelExecutionConfigurationStrategy`, a single method taking the `ConfigurationParameters` and returning a `ParallelExecutionConfiguration`. Use it when the number has to come from somewhere JUnit cannot see: a `CI_PARALLELISM` environment variable, the number of database schemas your test harness provisioned, a shard index, or a rule like "half the cores, but never more than six". ## Pool size, saturation and blocked tests Since JUnit 5.10 both built-in strategies expose two extra parameters — a maximum pool size (`…config.dynamic.max-pool-size-factor`, default 256, and `…config.fixed.max-pool-size`) and a saturate flag (`…config.dynamic.saturate` / `…config.fixed.saturate`, default `true`). They exist because the pool is a `ForkJoinPool`: when a test blocks — waiting on a lock, on I/O, on another test — the pool may compensate by starting an additional worker so that the *target* parallelism of running threads is maintained. Without headroom, a suite where many tests block can deadlock or crawl. Setting `saturate=false` makes the pool refuse to exceed its bounds, which is stricter but risks starvation. In practice you leave these alone until you observe a stall. The practical consequence to remember: **parallelism is a target for runnable threads, not a hard cap on threads**. If you must guarantee that no more than N tests touch an external system at once, enforce it with a semaphore or an exclusive-resource declaration, not by setting parallelism to N. ## Picking a number Start with `dynamic` and factor 1.0, measure, then adjust. Increase the factor while wall-clock time keeps dropping; stop when it plateaus or the failure rate rises. Switch to `fixed` once you want CI and local runs to behave identically or once the JVM's view of the CPU count is untrustworthy. Reach for `custom` only when the value must be derived at runtime from information outside JUnit.

  • Your tests are mostly HTTP calls to a stub server. Which strategy and value do you pick, and why?
    An I/O-bound suite leaves threads idle while waiting, so parallelism above the core count pays off: `dynamic` with a factor of 2–4, or a `fixed` value chosen by measurement. The limiting factor stops being CPU and becomes the stub server's own concurrency and any connection pool in the client, so I would raise the factor until wall-clock time stops improving and watch for timeouts appearing.
  • Why can the number of threads exceed the configured parallelism?
    The engine runs tests on a ForkJoinPool, where parallelism is the target number of *runnable* threads rather than a cap on total threads. When a test blocks — on I/O or waiting for an exclusive resource — the pool can compensate by adding a worker up to the maximum pool size so progress continues. If you need a hard cap on concurrent access to something, enforce it explicitly rather than relying on the parallelism value.

saying these in an interview costs you the question

  • Thinking the strategy alone enables parallel execution without the enabled flag and an execution mode
  • Treating parallelism as a hard upper bound on the number of threads
  • Forgetting that fixed.parallelism is mandatory for the fixed strategy
  • Assuming a higher dynamic factor always makes the suite faster
  • Believing availableProcessors() always reflects the container's CPU quota on CI

context