Em que Observer é diferente de Pub/Sub?

1. Visão geral

Observer e Pub/Sub resolvem o problema “um evento acontece, muitos lugares precisam saber”. Muitas pessoas confundem esses dois termos porque são semelhantes na ideia, mas diferem na implementação e no nível de acoplamento. Este artigo ajuda você a diferenciar claramente os dois conceitos, com código Java ilustrando cada um, e saber quando escolher Observer puro, quando usar Pub/Sub através de um message broker.

2. O que é o padrão Observer

Observer é um dos padrões de comportamento (behavioral pattern) clássicos do GoF. A ideia: um Subject (ou Publisher) mantém uma lista de Observers, quando o estado muda, o Subject chama diretamente o método em cada Observer registrado.

public interface OrderObserver {
    void onOrderPaid(Order order);
}

public class Order {
    private final List<OrderObserver> observers = new ArrayList<>();
    private String status = "NEW";

    public void addObserver(OrderObserver observer) {
        observers.add(observer);
    }

    public void markAsPaid() {
        this.status = "PAID";
        for (OrderObserver observer : observers) {
            observer.onOrderPaid(this);
        }
    }
}

Explicação:

  • observers é a lista de referências diretas aos objetos interessados.
  • markAsPaid() chama onOrderPaid() em cada observer em ordem, na mesma thread, de forma síncrona.
  • Order sabe exatamente quem está escutando — este é o ponto-chave para diferenciar de Pub/Sub.
public class EmailNotifier implements OrderObserver {
    @Override
    public void onOrderPaid(Order order) {
        System.out.println("Enviar email de confirmação para pedido pago");
    }
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
Enviar email de confirmação para pedido pago

2.1. Características principais do Observer

  • Acoplamento direto: Subject mantém referência ao Observer, precisam importar a interface um do outro.
  • Síncrono: a chamada notify bloqueia até que todos os observers terminem o processamento.
  • Mesmo processo: geralmente roda em uma JVM, não passa por rede.
  • Subject conhece observer: se esquecer de chamar removeObserver(), você facilmente terá vazamento de memória porque Subject mantém referência “viva” ao observer já descartado.

Nota: O padrão Observer clássico não tem mecanismo de unsubscribe seguro de forma nativa. Se você usar em aplicações de longa duração (por exemplo, componente UI, listener registrado e depois destruído), lembre-se de chamar removeObserver() quando o objeto for destruído, evitando vazamento de memória.

3. O que é Pub/Sub

Pub/Sub (Publish/Subscribe) separa completamente Publisher e Subscriber através de uma camada intermediária — chamada de broker, event bus ou topic. O Publisher não sabe quem está subscrito, apenas “dispara” a mensagem em um topic/canal.

public record OrderPaidEvent(String orderId, double amount) {}
public class OrderService {
    private final EventBus eventBus;

    public OrderService(EventBus eventBus) {
        this.eventBus = eventBus;
    }

    public void payOrder(String orderId, double amount) {
        // processa o pagamento...
        eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
    }
}

Explicação:

  • OrderService não sabe se EmailNotifier ou InventoryService existem ou não.
  • Apenas publica no topic "order.paid", o resto é responsabilidade do broker.
  • O subscriber se registra com o broker, não diretamente com OrderService.

3.1. Pub/Sub no processo com Spring Events

Spring Boot tem um event bus simples usando ApplicationEventPublisher. Este é o Pub/Sub em nível de aplicação, ainda sem broker externo como Kafka/RabbitMQ.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
public class OrderPaidEvent extends ApplicationEvent {
    private final String orderId;

    public OrderPaidEvent(String orderId) {
        super(orderId);
        this.orderId = orderId;
    }

    public String getOrderId() {
        return orderId;
    }
}
@Service
public class OrderService {
    private final ApplicationEventPublisher publisher;

    public OrderService(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void payOrder(String orderId) {
        publisher.publishEvent(new OrderPaidEvent(orderId));
    }
}
@Component
public class EmailEventListener {

    @EventListener
    public void handle(OrderPaidEvent event) {
        System.out.println("Enviar email para pedido " + event.getOrderId());
    }
}

Explicação:

  • OrderService publica o evento através de ApplicationEventPublisher — um bean gerenciado por Spring, que faz o papel de broker interno.
  • EmailEventListener não é conhecido por OrderService; Spring escaneia automaticamente métodos com @EventListener e roteia o evento ao handler correto baseado no tipo.
  • Por padrão, @EventListener roda síncrono na mesma thread do publisher — este é o ponto que confunde muita gente, pois pensam que Pub/Sub é sempre assíncrono.

Para rodar de forma assíncrona de verdade, adicione @Async:

@Component
public class EmailEventListener {

    @Async
    @EventListener
    public void handle(OrderPaidEvent event) {
        System.out.println("Enviar email para pedido " + event.getOrderId());
    }
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
  • @EnableAsync ativa o proxy para Spring rodar o método @Async em uma thread pool separada.
  • Neste caso payOrder() retorna imediatamente, sem esperar que EmailEventListener termine o processamento.

3.2. Pub/Sub por message broker (Kafka)

Em escala maior, o “broker” não é mais um bean dentro da JVM, mas um sistema separado (Kafka, RabbitMQ, SQS…). Publisher e subscriber podem estar em dois serviços, dois processos, duas máquinas diferentes.

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
@Service
public class OrderService {
    private final KafkaTemplate<String, OrderPaidEvent> kafkaTemplate;

    public OrderService(KafkaTemplate<String, OrderPaidEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void payOrder(String orderId, double amount) {
        kafkaTemplate.send("order-paid-topic", new OrderPaidEvent(orderId, amount));
    }
}
@Component
public class EmailConsumer {

    @KafkaListener(topics = "order-paid-topic", groupId = "email-service")
    public void consume(OrderPaidEvent event) {
        System.out.println("Serviço de email processando pedido " + event.orderId());
    }
}
  • OrderService e EmailConsumer podem estar em dois microserviços completamente diferentes, deployados independentemente.
  • Kafka mantém a mensagem no topic, o consumer lê na sua própria velocidade — este é o verdadeiro Pub/Sub, com buffer, replay, múltiplos subscribers independentes por grupo.

4. Comparação direta

Critério Observer Pub/Sub
Acoplamento Subject conhece Observer diretamente (pela interface) Publisher não sabe que Subscriber existe
Intermediário Nenhum, chama método direto Broker/event bus fica no meio
Síncrono/Assíncrono Por padrão síncrono, mesma thread Pode ser síncrono (Spring Event) ou assíncrono (Kafka, RabbitMQ)
Escopo Dentro de um processo/módulo Dentro do processo (event bus) ou entre múltiplos serviços/processos
Durabilidade de mensagem Nenhuma, se Subject morrer perde Broker externo (Kafka) armazena, permite replay
Número de publisher-subscriber Geralmente 1 Subject - múltiplos Observers Múltiplos Publishers - múltiplos Subscribers por múltiplos topics

A diferença mais importante: Observer é uma relação direta entre dois objetos, Pub/Sub é uma relação através de uma camada intermediária anônima. Essencialmente, Pub/Sub pode ser visto como o padrão Observer “afrouxado” inserindo um broker para eliminar a dependência direta.

5. Quando usar Observer, quando usar Pub/Sub

Use Observer quando:

  • A lógica fica no mesmo módulo/classe, número de observers é fixo e muda pouco.
  • Você precisa de resposta imediata, síncrona (por exemplo, validar dados antes de salvar).
  • Não precisa desacoplar demais — por exemplo, componente UI escutando mudanças de um modelo.
class ValidationObserver implements OrderObserver {
    @Override
    public void onOrderPaid(Order order) {
        if (order.getStatus() == null) {
            throw new IllegalStateException("Pedido ainda não tem status válido");
        }
    }
}

Use Pub/Sub quando:

  • Múltiplos serviços independentes precisam saber de um evento mas não devem depender um do outro.
  • Precisa da capacidade de adicionar/remover subscribers sem mudar o código do publisher.
  • Precisa de durabilidade de mensagem (se o subscriber estiver down, a mensagem espera quando ele ficar online novamente).
  • O sistema é microserviços, com comunicação pela rede.

Nota: Não use Kafka/RabbitMQ apenas para substituir uma List<Observer> dentro da mesma JVM — o custo de operar um broker não vale para um problema pequeno. Inversamente, não tente forçar lógica cross-service no padrão Observer puro, pois você terá que reinventar todo o mecanismo de rede, retry, serialização que o broker já oferece.

6. Testar comportamento do Observer

class OrderObserverTest {

    @Test
    void shouldNotifyAllObserversWhenOrderIsPaid() {
        Order order = new Order();
        List<String> notified = new ArrayList<>();

        order.addObserver(o -> notified.add("email"));
        order.addObserver(o -> notified.add("inventory"));

        order.markAsPaid();

        assertThat(notified).containsExactly("email", "inventory");
    }
}

Explicação:

  • Use lambda em vez de classe implementando OrderObserver porque a interface tem apenas um método — mais conciso ao escrever testes.
  • assertThat(...).containsExactly(...) confirma que a ordem de chamada dos observers é a mesma da ordem de registro — característica importante do Observer síncrono que Pub/Sub por broker não garante (Kafka pode processar em paralelo em múltiplas instâncias de consumer).

7. Erros comuns

  • Confundir Spring @EventListener com assíncrono: por padrão é síncrono, erro no listener lançará exceção de volta ao publisher se não for capturado.
  • Observer mantendo referência e esquecendo de remover: causa vazamento de memória, especialmente em aplicações com ciclo de vida curto de objetos (request-scope) mas registrados em singleton.
  • Abusar de Pub/Sub para lógica que precisa de transação consistente: se publisher e subscriber devem ambos ter sucesso ou fracasso (atomic), Pub/Sub assíncrono não é adequado — reconsidere padrão Saga ou transactional outbox.
  • Não nomear topics claramente: em sistemas grandes, topics com nomes vagos como "event1" fazem ninguém saber quem subscribe o quê, perdendo todo o benefício do desacoplamento.

8. Conclusão

Observer e Pub/Sub compartilham o mesmo objetivo — notificar múltiplas partes interessadas sobre mudanças — mas diferem no nível de acoplamento e escopo de operação. Observer é apropriado para relações diretas, síncronas, dentro do mesmo processo; Pub/Sub é apropriado quando você precisa desacoplar completamente o publisher do subscriber, especialmente em sistemas de microserviços que precisam de durabilidade de mensagem e capacidade de expansão com cada consumer independente. Entender bem esta diferença ajuda você a escolher a ferramenta certa: nem sempre você precisa de Kafka, e nem sempre uma List<Observer> é suficiente para seu problema.

design-patternobserverpub-subjavaspring-boot