@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;
}
}
@Service
public class OrderRecalculationService {
private static final long DEBOUNCE_MS = 750L;
private final OrderRepository orders;
private final PricingClient pricing;
private final OrderRecalculationService self;
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2, r -> new Thread(r, "order-debounce"));
private final ConcurrentMap<Long, ScheduledFuture<?>> pending = new ConcurrentHashMap<>();
public OrderRecalculationService(OrderRepository orders,
PricingClient pricing,
@Lazy OrderRecalculationService self) {
this.orders = orders;
this.pricing = pricing;
this.self = self; // proxy reference so @Async still applies
}
public void scheduleRecalculation(long orderId) {
pending.compute(orderId, (id, existing) -> {
if (existing != null) {
existing.cancel(false);
}
return scheduler.schedule(
() -> self.recalculate(id),
DEBOUNCE_MS,
TimeUnit.MILLISECONDS);
});
}
@Async("orderExecutor")
public void recalculate(long orderId) {
pending.remove(orderId);
Order order = orders.findById(orderId).orElse(null);
if (order == null) {
return;
}
BigDecimal subtotal = order.getLines().stream()
.map(l -> l.getUnitPrice().multiply(BigDecimal.valueOf(l.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
order.setSubtotal(subtotal);
order.setTax(pricing.taxFor(order));
order.setShipping(pricing.shippingFor(order));
order.setTotal(subtotal.add(order.getTax()).add(order.getShipping()));
orders.save(order);
}
@PreDestroy
public void shutdown() {
scheduler.shutdown();
}
}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderRepository orders;
private final OrderRecalculationService recalculation;
public OrderController(OrderRepository orders, OrderRecalculationService recalculation) {
this.orders = orders;
this.recalculation = recalculation;
}
@PatchMapping("/{id}/lines")
public ResponseEntity<Void> updateLines(@PathVariable long id,
@RequestBody @Valid OrderLinesPatch patch) {
Order order = orders.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
order.applyLinePatch(patch);
orders.save(order);
recalculation.scheduleRecalculation(id);
return ResponseEntity.accepted().build();
}
}
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
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.