Desacoplamento e Reatividade: Implementando o Design Pattern Observer com Spring Events
Source: Dev.to
Introdução
Em nossa jornada como desenvolvedores, frequentemente nos deparamos com a necessidade de orquestrar múltiplas ações a partir de um único evento central. Esse é 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 do 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. Assim que essa resposta chega, várias operações de follow‑up precisam ser disparadas simultaneamente e de forma independente:
| Ação | Descrição |
|---|---|
| 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.: serviço de Notificações Assíncronas). |
O problema aqui não é apenas executar as ações, mas fazê‑lo sem que a classe 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 escolhida foi o Padrão Observer.
Observer – Visão geral
O Observer é um padrão comportamental que define uma dependência um‑para‑muitos entre objetos:
- Subject (Publisher) – notifica todos os seus dependentes quando há alteração de estado.
- Observers (Subscribers) – 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.
1️⃣ O Evento (ApplicationEvent)
Criamos o objeto que encapsula os dados a serem transmitidos. Ele deve estender 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 final ApplicationEventPublisher publisher;
public PaymentService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void processPayment(PaymentResponse response) {
publisher.publishEvent(new PaymentEvent(this, response));
}
}
3️⃣ Os Listeners (Observers)
As classes EmailListener e NotificationListener tornam‑se nossos observadores. Utilizamos duas anotações poderosas para o desacoplamento:
@EventListener– marca o método como ouvinte para o tipo de evento específico.@Async– garante que o listener será executado em uma thread separada, liberando a thread principal do request de pagamento.
Email Listener
@Component
@Slf4j
public class EmailListener {
@Async
@EventListener
public void handlePayment(PaymentEvent event) {
log.info("Email sent for payment: {}", event.getPaymentResponse().paymentStatus());
}
}
Notification Listener
@Component
@Slf4j
public class NotificationListener {
@Async
@EventListener
public void handlePayment(PaymentEvent event) {
log.info("Notification sent 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, no futuro, for necessário que a notificação seja processada antes do e‑mail, podemos forçar a ordem utilizando a anotação @Order. O Spring respeitará a prioridade definida.
@Component
@Slf4j
@Order(1) // Executa antes de listeners com ordem maior
public class NotificationListener { … }
@Component
@Slf4j
@Order(2)
public class EmailListener { … }
Listener transacional
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 que o listener só será disparado após o commit efetivo. Se o commit falhar (rollback), o evento nunca será disparado, prevenindo o envio de e‑mails sobre pagamentos que nunca foram registrados.
@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
}
}
Conclusão
Utilizando Spring Events conseguimos:
- Desacoplar o fluxo de pagamento das ações de follow‑up.
- Adicionar novos listeners sem tocar no
PaymentService(princípio Open/Closed). - Controlar a ordem de execução e garantir consistência transacional.
- Escalar a solução de forma simples, aproveitando o suporte nativo do Spring a
@Async,@Ordere@TransactionalEventListener.
O padrão Observer, aliado ao ecossistema Spring, prova ser uma solução elegante e robusta para orquestrar múltiplas ações a partir de um único evento central. 🚀
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 devem ser consideradas.
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: Defina um
AsyncConfigcom limites controlados de threads e umRejectedExecutionHandleradequado 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();
// Pool limitado: 4 threads sempre vivos
executor.setCorePoolSize(4);
// Máximo de 8 threads sob carga
executor.setMaxPoolSize(8);
// FILA LIMITADA: evita overload
executor.setQueueCapacity(25); // Máximo de 25 tarefas na fila
// Prefixo para depuração (ex.: Async-1, Async-2...)
executor.setThreadNamePrefix("Payment-Async-");
// Handler: rejeita tarefas extras (não usa fila infinita)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// Shutdown: aguarda tarefas terminarem
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 implica:
- Escalabilidade natural limitada. Em cenários de altíssimo volume (por exemplo, > 200 k requisições/minuto), a sobrecarga do thread pool torna‑se um gargalo.
- Não resolve comunicação entre microsserviços. Em arquiteturas distribuídas, ele não substitui um broker de mensagens.
Para casos de extrema escala ou arquitetura distribuída, a solução ideal é migrar para um Message Broker dedicado (Kafka, RabbitMQ, SQS, etc.).
Conclusão
O Spring Event é uma ferramenta de produtividade fantástica que permite implementar o padrão Observer de forma simples e gerenciável. Ele é ideal para:
- Desacoplar ações secundárias.
- Dar reatividade aos fluxos dentro de um mesmo serviço.
Entretanto, para garantir robustez e escalabilidade, é imprescindível:
- Configurar corretamente os aspectos assíncronos (pools, handlers, time‑outs).
- Reconhecer os limites de escalabilidade dos eventos locais.
- Optar por brokers de mensagens quando a comunicação precisar atravessar limites de processo ou serviço.
Com essa estrutura e esses cuidados, seu sistema ficará mais robusto, escalável (dentro do contexto do serviço) e, o mais importante, fácil de manter e evoluir.
Até a próxima!