java 96 lines · 3 tabs

Debouncing Order Recalculations with Spring @Async and a Configured ThreadPoolTaskExecutor

Shared by codesnips Jul 2026
3 tabs
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "orderExecutor")
    public ThreadPoolTaskExecutor orderExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("order-recalc-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }
}
3 files · java Explain with highlit

This snippet shows how a burst of order edits can be collapsed into a single expensive recalculation using Spring's @Async support backed by a dedicated ThreadPoolTaskExecutor. The problem it solves is common: rapid successive changes to an order (line items added, quantities tweaked) each want to trigger a full recompute of totals, tax, and shipping, but running that recompute on every keystroke wastes CPU and hammers downstream services. Debouncing coalesces those events so only the last one within a quiet window actually runs.

In AsyncConfig, a named bean orderExecutor defines the pool: a small corePoolSize, a bounded queueCapacity, and a CallerRunsPolicy rejection handler so back-pressure falls back to the calling thread rather than dropping work. @EnableAsync wires Spring's proxy-based async interception, and giving the executor an explicit bean name lets @Async("orderExecutor") target it precisely instead of using the shared default SimpleAsyncTaskExecutor, which spawns unbounded threads.

OrderRecalculationService implements the debounce itself. A ConcurrentHashMap of ScheduledFuture per order id tracks the pending run. scheduleRecalculation cancels any in-flight timer for that order and schedules a fresh one on a ThreadScheduledExecutor, so only the final call in a rapid sequence survives the DEBOUNCE delay. When the delay elapses, the scheduled task hands off to recalculate, the @Async("orderExecutor") method, so the actual heavy work runs on the tuned pool rather than the single scheduler thread. The cancel(false) call lets an already-started recompute finish rather than interrupting mid-flight.

OrderController is the trigger surface: a PATCH endpoint updates the order and calls scheduleRecalculation, returning 202 Accepted immediately since the recompute is deferred. The trade-off is eventual consistency — the client sees stale totals for up to the debounce window — which is acceptable for a UI that polls or receives a push once the async result lands. A pitfall worth noting is that @Async self-invocation does not go through the proxy, which is why the scheduling and the async method live so the timer callback invokes the injected bean. Bounding the queue and choosing a sane rejection policy keeps the service stable under load spikes.


Related snips

Share this code

Here's the card — post it anywhere.

Debouncing Order Recalculations with Spring @Async and a Configured ThreadPoolTaskExecutor — share card
Link copied