One property, and every request plus every @Async task runs on a virtual thread:
spring.threads.virtual.enabled=true
Blocking code stops being expensive: a thread parked on I/O costs almost nothing, so an MVC
app handles thousands of concurrent requests without going reactive.
Two rules
Do not pool virtual threads — create one per task. And avoid synchronized around blocking
calls; use ReentrantLock instead.
@ComponentclassJobs{@Scheduled(fixedRate=60_000)// every minute, from start to startvoidpoll(){…}@Scheduled(fixedDelay=5_000,initialDelay=10_000)// 5 s after the last one endedvoiddrainQueue(){…}@Scheduled(cron="0 0 3 * * *",zone="Europe/Berlin")// 03:00 dailyvoidnightlyCleanup(){…}}
Cron fields: second minute hour day-of-month month day-of-week.
@ComponentclassSearchIndexer{@EventListenervoidon(BookCreatedevent){…}// synchronous, same transaction@Async@TransactionalEventListener// only after a successful commitvoidindex(BookCreatedevent){…}}