Skip to main content

Inject the executor

advanced15 min readLesson 165 of 180

Why commonPool is not yours, and how injected executors make concurrency a deployment decision.

CompletableFuture.supplyAsync(fn) runs on the ForkJoinPool.commonPool — a shared, sized-to-cores pool you did not configure and cannot tune from inside the task. Under load, unrelated features share (and starve) it. The advanced move: inject the executor.

CompletableFuture.supplyAsync(() -> fetch(id), executor);

Now the pipeline's concurrency is a deployment decision, visible at the construction site. In tests, inject an executor you control (a fixed pool of 2 makes "these two ran concurrently" observable and deterministic). In production, inject a pool sized for the workload. The anti-pattern is a library that hard-wires commonPool deep inside and gives the caller no say.

Executor choice is also where virtual threads re-enter: for blocking-dominated fan-outs, Executors.newVirtualThreadPerTaskExecutor() is the injected executor that scales.