Em nossa jornada como desenvolvedores, frequentemente nos deparamos com a necessidade de orquestrar múltiplas ações a partir de um único evento central. Este é um desafio clássico que, se não for abordado corretamente, pode levar a um alto acoplamento e à temida "bola de lama" de código.
Neste post, vou compartilhar um cenário real de pagamento em um sistema e como o Design Pattern Observer, implementado através do Spring Event, se mostrou a solução elegante e robusta para garantir a reatividade e o desacoplamento de nosso fluxo. O Problema: Acionamento Múltiplo e Consistência.
Imagine o fluxo de processamento de um pagamento. A etapa final retorna um status central: Success, Fail ou Pending. Com a chegada dessa resposta, várias operações de follow-up precisam ser disparadas simultaneamente e de forma independente:
- Notificação: Enviar um e-mail de confirmação ou erro ao cliente.
- Integração: Gerar um push de notificação para uma fila externa (ex: para processamento em um serviço de Notificações Assíncronas).
O problema aqui não é apenas executar as ações, mas fazê-lo sem que a classe de PaymentService precise conhecer ou ser responsável por cada uma delas. A solução deve permitir adicionar novas ações no futuro sem alterar o código principal do pagamento, seguindo o princípio Open/Closed do SOLID. A Solução Arquitetural escolhido foi o Padrão Observer.
O Observer é um padrão comportamental que define uma dependência um para muitos entre objetos.
- Um objeto (Subject ou Publisher) notifica todos os seus dependentes (Observers ou Subscribers) sobre qualquer alteração de estado.
- Os dependentes são notificados e reagem à mudança, sem que o Subject saiba a identidade ou o propósito específico de cada um.
Identificamos que essa era a abstração perfeita para o nosso cenário.
Implementação Prática com Spring Events.
No ecossistema Spring Boot, a maneira mais idiomática e gerenciada de implementar o padrão Observer é através dos Spring Events. O framework assume a responsabilidade pela orquestração do padrão, simplificando drasticamente a implementação.
Para isso, definimos três componentes principais:
1. O Evento (ApplicationEvent):
Criamos o objeto que encapsula o dado a ser transmitido. Ele deve estender de ApplicationEvent, permitindo que o Spring Context o monitore.
public class PaymentEvent extends ApplicationEvent {
private final PaymentResponse paymentResponse;
public PaymentEvent(Object source, PaymentResponse paymentResponse) {
super(source);
this.paymentResponse = paymentResponse;
}
public PaymentResponse getPaymentResponse() {
return paymentResponse;
}
}
2. O Publicador (ApplicationEventPublisher):
O serviço que conclui o pagamento (nosso Subject) passa a ter uma dependência no publisher do Spring. É ele quem realiza o disparo no momento crucial do fluxo.
@Service
public class PaymentService {
private ApplicationEventPublisher publisher;
public PaymentService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void processPayment(PaymentResponse response) {
publisher.publishEvent(new PaymentEvent(this, response));
}
}
3. Os Listeners (Os Observers):
As classes de NotificationListener e EmailListener se tornam os nossos observadores. Utilizaremos duas anotações poderosas para o desacoplamento:
@EventListener: Marca o método como um ouvinte para o tipo de evento específico.
@async: Crucial para a performance. Garante que o Listener será executado em uma thread separada, liberando a thread principal do request de pagamento para finalizar rapidamente.
@Component
@Slf4j
public class EmailListener {
@Async
@EventListener
public void handlePayment(PaymentEvent event) {
log.info("Email send for payment: " + event.getPaymentResponse().paymentStatus());
}
}
@Component
@Slf4j
public class NotificationListener {
@Async
@EventListener
public void handlePayment(PaymentEvent event) {
log.info("Notification send for payment: " + event.getPaymentResponse().paymentStatus());
}
}
Controle Fino e Consistência Transacional
Para um projeto não basta apenas que funcione; precisamos de controle e garantia de consistência. O Spring Event oferece recursos para isso.
Ordem de Execução
Se, em um cenário futuro, for necessário que a Notificação seja processada antes do E-mail, podemos forçar a ordem utilizando a anotação @Order(1) (ou qualquer número) no método listener. O Spring respeitará a ordem de prioridade definida. O Listener Transacional @TransactionalEventListener faz parte de um dos maiores desafios em sistemas distribuídos é garantir que as ações reativas (como enviar um e-mail) só ocorram se a transação do banco de dados (o commit do pagamento) for bem-sucedida.
É aqui que entra o @TransactionalEventListener. Ao utilizá-lo com phase = TransactionPhase.AFTER_COMMIT, garantimos queo listener só será disparado após a transação de banco de dados ser efetivamente comitada. Se o commit falhar (um rollback), o evento nunca será disparado, prevenindo o envio de e-mails sobre pagamentos que nunca foram registrados no banco.
@Component
@Slf4j
public class OtherListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handlePaymentSuccess(PaymentSuccessEvent event) {
log.info("Other notification: {}", event.getPayment().getUserEmail());
// Ex: push SMS
}
}
Considerações de Escalabilidade e Resiliência
Embora o Spring Event seja uma ferramenta excelente para o desacoplamento dentro do mesmo serviço (monolito ou microsserviço), há ressalvas importantes que se deve considerar.
Gerenciamento de Threads Assíncronas
O uso de @async consome threads do pool interno do Spring. Sem a configuração correta de um pool dedicado (AsyncConfig), podemos ter um cenário de Thread Pool Exhaustion.
- Risco: Estratégias de retry mal planejadas ou listeners que demoram muito para executar podem exaurir o pool, levando a thread locks e, em casos extremos, à queda do sistema.
- Mitigação: É fundamental definir um AsyncConfig com limites controlados de threads e um RejectedExecutionHandler adequado para lidar com a sobrecarga.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
@Bean(name = "taskExecutor") // Name for @Async("taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Limited pool: 4 always-living threads
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8); // Max 8 threads under load
// LIMITED QUEUE: avoids overload
executor.setQueueCapacity(25); // Maximum 25 tasks in the queue
// Prefix for debugging (see logs: Async-1, Async-2...)
executor.setThreadNamePrefix("Payment-Async-");
// Handler: rejects extra tasks (not an infinite queue)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// Shutdown: wait for tasks to finish
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
Limitações de Volume e Arquitetura
É crucial entender que o Spring Event é uma solução de evento local (In-Process Messaging). Isso significa que:
- Ele tem um limite natural de escalabilidade. Em cenários de altíssimo volume (como acima de 200K requisições/minuto), a sobrecarga do thread pool se torna um gargalo.
- Ele não resolve problemas de comunicação entre microsserviços (arquiteturas distribuídas).
Nesses casos de extrema escala ou arquitetura distribuída, a solução ideal migra para o uso de um Message Broker dedicado (como Kafka, RabbitMQ ou SQS).
Conclusão
O Spring Event é uma ferramenta de produtividade fantástica que permite implementar o poderoso Design Pattern Observer de forma simples e gerenciável. Ele é ideal para desacoplar ações secundárias e dar reatividade aos fluxos dentro de um mesmo serviço. No entanto, é importante ir além do uso básico, configurando corretamente os aspectos assíncronos e entendendo os limites de sua escalabilidade e a diferença entre eventos locais e arquiteturas de mensageria distribuída.
Com essa estrutura e esses cuidados, seu sistema estará mais robusto, escalável (dentro do contexto do serviço) e, o mais importante, fácil de manter e evoluir.
Até a próxima!
Top comments (0)