Cuidados com @Async em sistemas distribuídos cheios de integrações
O @Async devolve o controle rápido, mas troca a garantia de execução por uma promessa em memória que some quando o pod cai.
Colocar @Async num método é a forma mais barata de "deixar para depois". Em um sistema com muitas integrações, esse depois tem custo.
O que você perde sem perceber
Durabilidade. A tarefa vive numa fila em memória do pod. Se o pod reiniciar em um deploy ou por falta de memória, o trabalho pendente some, sem erro e sem registro.
Exceções. Em métodos void, a exceção não chega a quem chamou. Sem um AsyncUncaughtExceptionHandler, ela vira apenas uma linha de log.
@Async
public void notificarParceiro(Pedido p) {
parceiroClient.enviar(p); // falhou? ninguém sabe
}
Contexto. Usuário autenticado, transação, MDC de log e correlation id ficam na thread original. Na thread assíncrona eles não existem, a menos que você os propague.
O pool é parte do design
Se você não configurar, o Spring Boot fornece um executor padrão, com fila praticamente ilimitada. Sob carga, a fila cresce até a memória acabar, em vez de aplicar contrapressão. Configure um pool com limites e uma política de rejeição explícita:
@Bean
public Executor integracaoExecutor() {
var ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(8);
ex.setMaxPoolSize(16);
ex.setQueueCapacity(200);
ex.setThreadNamePrefix("integracao-");
ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return ex;
}
Use pools separados por integração. Uma integração lenta não deve esgotar as threads das outras.
Quando @Async não é a ferramenta
Se perder a tarefa é inaceitável, o trabalho precisa passar por algo durável: uma fila de mensagens, ou o padrão outbox. O @Async serve para trabalho descartável ou refazível, como cache e métricas.
Checklist
- O trabalho pode ser perdido? Se não, use fila.
- Quem trata a falha? Ela é observável?
- O pool tem limite e política de rejeição?
- O contexto necessário está sendo propagado?
- A operação é idempotente para o caso de reexecução?