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

Published: (February 17, 2026 at 02:05 PM EST)
7 min read
Source: Dev.to

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çãoDescrição
NotificaçãoEnviar um e‑mail de confirmação ou erro ao cliente.
IntegraçãoGerar 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, @Order e @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 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();

        // 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:

  1. Configurar corretamente os aspectos assíncronos (pools, handlers, time‑outs).
  2. Reconhecer os limites de escalabilidade dos eventos locais.
  3. 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!

0 views
Back to Blog

Related posts

Read more »

Scanner Class

Class in Java Definition of the class concept in Java. Why Scanner? The Scanner class simplifies reading input from various sources, such as the keyboard. How...