DEV Community

Cover image for Desacoplamento e Reatividade: Implementando o Design Pattern Observer com Spring Events
Wagner Negrão 👨‍🔧
Wagner Negrão 👨‍🔧

Posted on

Desacoplamento e Reatividade: Implementando o Design Pattern Observer com Spring Events

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:

  1. Notificação: Enviar um e-mail de confirmação ou erro ao cliente.
  2. 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode
@Component
@Slf4j
public class NotificationListener {
    @Async
    @EventListener
    public void handlePayment(PaymentEvent event) {
        log.info("Notification send for payment: " + event.getPaymentResponse().paymentStatus());
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)