Spring Boot / Intermediate: data, security, and framework evidence / Lesson 12
Async work, scheduling, and event lifecycle
Define task admission, execution, context, failure, and shutdown rather than treating Async or Scheduled as durable background-processing guarantees.
On this page
Lesson goals
- Bound executor work and observe asynchronous outcomes.
- Explain proxy and context-propagation limits.
- Separate local events and schedules from durable delivery.
Acceptance and completion are different outcomes
@Async moves an eligible managed method invocation onto a selected executor; it does not make a task durable or prove success when the caller returns. Define admission limits, queue capacity, result/failure observation, timeout meaning, cancellation, and ownership. An HTTP202 contract should say how accepted work is tracked rather than pretending the business operation already completed.
The usual proxy boundary applies: self-invocation can bypass async advice, and a manually constructed object is not automatically intercepted. A method returning void needs an explicit uncaught-failure policy, while a future can still hide errors if nobody observes it. Keep request data as a bounded immutable command, not a retained servlet request or a mutable global field.
Select a named bounded executor deliberately
The fragment enables async support and defines a named ThreadPoolTaskExecutor with finite worker and queue limits. The async method performs the contextual builder operation on that executor and returns its result, without adding a second hidden common-pool dispatch. It is not executed, and the application must define rejection and shutdown behavior for its workload.
A default unbounded queue can defeat assumptions based on maxPoolSize alone. Saturation should produce a documented backpressure or rejection outcome rather than infinite resubmission. Worker capacity also must respect downstream limits such as database connections. More threads do not create more external capacity or repair a lock cycle. Keep resources and deadlines coherent across the whole operation.
package training.catalog;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
@Configuration(proxyBeanMethods = false)
@EnableAsync
class AsyncConfiguration {
@Bean("catalogTasks")
ThreadPoolTaskExecutor catalogTasks() {
var executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(32);
executor.setThreadNamePrefix("catalog-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return executor;
}
}
@Service
class SummaryJobs {
private final SummaryBuilder builder;
SummaryJobs(SummaryBuilder builder) { this.builder = builder; }
@Async("catalogTasks")
public CompletableFuture<Summary> build(SummaryCommand command) {
return CompletableFuture.completedFuture(builder.build(command));
}
}
Spring lifecycle initializes the executor when integrated. No bean, worker, scheduler, or background service is started from this fragment here.
Observe one result and release owned workers
The complete Java17 model submits one bounded pure task, retrieves7, and shuts down the owned executor in finally. It validates result observation and lifecycle mechanics without using Spring, listeners, external systems, or load. It does not validate @Async interception, task decorators, or the actual framework rejection translation. Those need separate scoped evidence.
Waiting with a timeout limits the caller's wait, not automatically the task's effects. Interruption is cooperative: propagate InterruptedException where allowed or restore the flag when necessary, and release resources. A task that ignores cancellation can outlive a request. Do not use abrupt thread termination as a routine lifecycle strategy or assume every canceled request means database rollback.
public class Main {
public static void main(String[] args) throws Exception {
var executor=new java.util.concurrent.ThreadPoolExecutor(1,1,0L,java.util.concurrent.TimeUnit.MILLISECONDS,
new java.util.concurrent.ArrayBlockingQueue<Runnable>(1));
try {
var result=executor.submit(()->7);
System.out.println(result.get());
} finally {
executor.shutdown();
if(!executor.awaitTermination(2,java.util.concurrent.TimeUnit.SECONDS)) executor.shutdownNow();
}
}
}
7
One local result is observed and the worker is terminated. No framework task execution or saturation benchmark is claimed.
Do not assume context or durability crosses a thread
Transaction state, security context, logging MDC, and request-scoped objects do not become correctly propagated merely because a method is async. Use a reviewed minimal context strategy or framework-specific wrapper, and restore/clear context after execution to avoid leaking one request into another. A new worker may need its own deliberate transaction rather than an inherited illusion.
Ordinary application events are local process coordination; listener behavior depends on multicaster and async configuration. TransactionalEventListener phases can align a callback with a transaction, but after-commit notification is not a durable outbox. A crash can lose unpersisted work. Scheduled work can run on every application instance, and restart/misfire/duplicate behavior needs a separate reliability design.
Test lifecycle and failure without timing guesses
Use controlled collaborators, latches where appropriate, and bounded waits to test overlap, rejection, failure, cancellation, and shutdown. Do not sleep until a background result usually appears and call the test deterministic. Verify request state does not leak and every failure reaches an observable channel. For durable work, inspect persisted job/event state rather than a log line saying a task was submitted.
No scheduler or application context is started here. Keep framework fixtures non-runnable until tested in an authorized isolated environment with their actual dependency graph. A reliable async boundary states what acceptance means, when completion is known, which context is trusted, who owns resources, and how the operation survives or deliberately does not survive process failure.
Common errors and fixes
TaskRejectedException
- Why it happens
- The selected executor cannot admit work under its current capacity/lifecycle.
- How to fix it
- Apply the documented rejection/backpressure policy.
- Prevent it next time
- Test bounded saturation and shutdown behavior in isolation.
An async failure disappeared
- Why it happens
- A void method or ignored future had no observed failure path.
- How to fix it
- Define and verify result/exception observation.
- Prevent it next time
- Test a failing task, not only success.
A scheduled operation ran twice across replicas
- Why it happens
- Local scheduling was mistaken for globally single execution.
- How to fix it
- Use a reviewed durable coordination and idempotency contract.
- Prevent it next time
- Test multi-instance assumptions without real service load.
Check your understanding
1. Does202 or a returned Future prove a summary finished?
Show worked solution
No. It can represent acceptance or a handle to eventual completion. Define durable status if required, observe success/failure, bound waiting, and distinguish timeout from canceled or completed work. A submitted task may still fail after the request returns.
2. Why is an after-commit local event not automatically a durable delivery guarantee?
Show worked solution
The callback timing can follow a transaction, but unpersisted processing can still be lost on process failure and delivery may repeat under another design. Persist an outbox/job identity and define consumer deduplication when durable business delivery is required.
Key takeaways
- Async acceptance, completion, and durable processing differ.
- Executors need bounded admission, context, and lifecycle ownership.
- Local events and schedules are not a substitute for recovery design.