Serguey Asael Shinder: A thread with no name costs you the next production stack dump
Take a thread dump of almost any Java service and count how many lines look like this:
"pool-2-thread-1" #34 prio=5 waiting on condition
"pool-2-thread-2" #35 prio=5 waiting on condition
"pool-3-thread-7" #61 prio=5 runnable
Which pool is pool-3? Nobody knows. The name is assigned by Executors.defaultThreadFactory(), counting up from one across the whole JVM in creation order, and creation order depends on which beans initialised first. It is stable enough to look meaningful and arbitrary enough to be useless.
The fix is one argument
ExecutorService ingest = Executors.newFixedThreadPool(
8, Thread.ofPlatform().name("ingest-", 0).factory());
Now the dump reads ingest-0 … ingest-7, and the same strings appear in your logs if the pattern includes %thread, in the profiler, in jcmd Thread.print, and in the flight recorder. The Thread.ofPlatform() builder has been there since Java 21; before that, three lines of ThreadFactory do the same job.
Virtual threads take a name too, and it matters more there, because there are a great many of them:
Thread.ofVirtual().name("req-", 0).start(task);

Where it pays
In a thread dump you did not plan for. The dump you take during an incident is the one you get — there is no second chance to add instrumentation. If half the threads are pool-N-thread-M, deciding whether the stuck ones belong to the HTTP layer or the batch importer is guesswork done under time pressure.
In lock contention. "ingest-3" waiting to lock <0x...> held by "report-0" is a sentence about your architecture. The same line with pool names is a sentence about the JVM.
In metrics. Any per-thread metric keyed on the name — CPU time, allocation, blocked time — becomes groupable by subsystem for free, because the prefix is the subsystem.
Two details that bite
Name the pool at the factory, not inside the task. Setting Thread.currentThread() .setName(...) at the top of a task works and then leaks: the pool reuses the thread, the name from the last task survives, and the dump shows one job while a different one runs. If you do rename per task, rename back in a finally.
Keep the prefix short and greppable. ingest- is better than com.example.service.ingest.executor-. The name shows up in fixed-width columns in half the tools that display it, and a long prefix means the digit that distinguishes two threads is the part that gets cut off.
The whole change is one argument per executor, applied once, in the place where the pool is built. It costs nothing at runtime and it is worth an hour of the next incident.