@Async Caveats
@Async on a Spring bean method causes it to execute asynchronously on a separate thread (by default the SimpleAsyncTaskExecutor, which creates a new thread per invocation). Returns must be void or Future/CompletableFuture.
@Async = dropping a letter in a mailbox for later delivery. But self-invocation is handing the letter to yourself -- you just read it immediately. No mailman, no async, no new thread.
Like @Transactional, @Async relies on the AOP proxy — self-invocation bypasses it and the method runs synchronously. SimpleAsyncTaskExecutor creates an unbounded number of threads — in production, always configure a custom ThreadPoolTaskExecutor (via @EnableAsync + @Bean TaskExecutor). The SecurityContext is NOT automatically propagated to async threads — use DelegatingSecurityContextExecutorService or store/restore the context manually. Exceptions thrown from a void @Async method are lost unless an AsyncUncaughtExceptionHandler is configured.
CompletableFuture returned by @Async is not backed by the ForkJoinPool.commonPool() — it uses the configured task executor. Callers can still chain .thenApply() etc. Exception handling: for Future-returning async methods, the exception is captured in the Future and re-thrown on get(). For void methods, without an AsyncUncaughtExceptionHandler, the exception is logged and silently dropped. @Async and @Transactional can coexist on the same method but require careful ordering — @Async creates a new thread (so the transaction from the caller thread doesn't propagate), which is almost always what you want (a transaction per async task).
@Async dispatches the annotated method to a thread pool (configure a custom ThreadPoolTaskExecutor — the default creates a new thread per call and is unsuitable for production). Self-invocation bypasses the proxy, so the method runs synchronously. Exceptions from void @Async methods are silently swallowed without an AsyncUncaughtExceptionHandler. The Spring SecurityContext is not automatically inherited by async threads — explicitly propagate it via DelegatingSecurityContextRunnable or a custom executor decorator.
Calling an @Async method on this (self-call) within the same bean executes synchronously — no new thread is created, no proxy intercepts the call — and there is no compile-time or runtime warning.