Skip to content

Reboredo.bit

Menu
  • Início
  • Sobre mim
  • Posts
Menu

Publish–Subscribe em Java: como implementar o padrão PubSub passo a passo

Posted on 19 de fevereiro de 202619 de fevereiro de 2026 by victoreboredo.dev@gmail.com

Em sistemas distribuídos modernos, especialmente em arquiteturas baseadas em microsserviços, o desacoplamento entre componentes é um dos principais fatores que determinam a escalabilidade, a manutenibilidade e a evolução de uma plataforma. À medida que sistemas crescem em complexidade, aumenta também o risco de interdependências rígidas entre serviços, tornando mudanças mais difíceis e aumentando o custo de manutenção.
O acoplamento ocorre quando um componente depende diretamente de outro para funcionar. Esse tipo de dependência cria uma relação forte entre produtor e consumidor de dados, dificultando a evolução independente de cada parte do sistema. Em arquiteturas modernas, o objetivo é reduzir ao máximo esse acoplamento, permitindo que componentes evoluam, sejam substituídos ou escalados sem impactar o restante do sistema.


Comunicação direta e síncrona nos primeiros sistemas

Nos primeiros sistemas computacionais, especialmente entre as décadas de 1960 e 1970, a comunicação entre módulos era predominantemente direta e síncrona. Um componente precisava conhecer explicitamente o outro componente com o qual se comunicava, estabelecendo uma dependência direta entre ambos.

Esse modelo era adequado para sistemas menores, onde o número de componentes era limitado e as relações entre eles eram relativamente simples.

No entanto, conforme os sistemas cresceram em tamanho e complexidade, esse modelo passou a apresentar limitações significativas. A necessidade de conhecer explicitamente outros componentes aumentava o acoplamento, dificultando modificações, testes e extensões.

O surgimento da arquitetura orientada a eventos

Com o avanço dos sistemas distribuídos e o surgimento de interfaces gráficas orientadas a eventos na década de 1980, tornou-se necessário adotar modelos mais flexíveis de comunicação. Nesse contexto, emergiu o paradigma da arquitetura orientada a eventos (Event-Driven Architecture).

Nesse modelo, componentes passam a se comunicar por meio de eventos, que representam mudanças de estado ou ocorrências relevantes no sistema. Em vez de invocar diretamente outro componente, um serviço simplesmente emite um evento, e outros componentes interessados podem reagir a esse evento de forma independente.

Essa abordagem reduz o acoplamento, melhora a extensibilidade e permite que novos comportamentos sejam adicionados ao sistema sem modificar os componentes existentes.

O padrão Publish–Subscribe como solução arquitetural

O padrão Publish–Subscribe, também conhecido como PubSub, é uma das implementações mais importantes do paradigma orientado a eventos. Nesse modelo, existem três elementos principais: o publisher, o subscriber e um intermediário, geralmente chamado de broker, event bus ou dispatcher.

O publisher é responsável por publicar eventos, enquanto o subscriber se registra para receber eventos de seu interesse. O broker atua como intermediário, recebendo eventos dos publishers e distribuindo-os aos subscribers apropriados.

Uma característica fundamental desse modelo é que publishers e subscribers não possuem conhecimento direto um do outro. O publisher não precisa saber quem irá consumir o evento, e o subscriber não precisa saber quem o produziu. Essa separação reduz significativamente o acoplamento e permite que componentes sejam adicionados, removidos ou modificados sem impactar o restante do sistema.

Esse nível de indireção é um dos principais fatores que tornam o padrão Publish–Subscribe altamente escalável e adequado para sistemas distribuídos.

Relação com o padrão Observer

O padrão Publish–Subscribe compartilha sua motivação fundamental com o padrão Observer, ambos buscando desacoplar produtores e consumidores de informação. No padrão Observer, um objeto chamado Subject mantém uma lista de Observers e os notifica diretamente quando ocorre uma mudança de estado.

Esse padrão é amplamente utilizado dentro de aplicações orientadas a objetos, especialmente para lidar com eventos internos, como atualizações de interface gráfica, mudanças em modelos de domínio ou mecanismos de callback.

No entanto, existe uma diferença estrutural importante entre os dois padrões. No Observer, o Subject mantém uma referência direta aos Observers, mesmo que por meio de interfaces. Isso significa que ainda existe um certo nível de acoplamento estrutural entre as partes.

No padrão Publish–Subscribe, essa relação é mediada por um broker. Nenhuma das partes precisa manter referências diretas umas às outras. Esse modelo permite comunicação entre diferentes processos, serviços ou até mesmo máquinas distintas, tornando-o mais adequado para arquiteturas distribuídas.

Nesse sentido, o padrão Observer pode ser visto como uma implementação local do mesmo princípio fundamental, enquanto o Publish–Subscribe representa sua evolução para ambientes distribuídos.

A consolidação do PubSub em sistemas modernos

A partir das décadas de 1990 e 2000, o padrão Publish–Subscribe ganhou ampla adoção com o surgimento de middleware de mensagens e tecnologias como o Java Message Service (JMS), que formalizaram esse modelo no ecossistema corporativo.

Com o crescimento das arquiteturas baseadas em microsserviços e sistemas orientados a eventos, o padrão tornou-se um dos pilares fundamentais da comunicação moderna. Atualmente, ele é amplamente utilizado em plataformas como Apache Kafka, RabbitMQ e outros sistemas de streaming de eventos.

Essas tecnologias permitem que sistemas processem grandes volumes de dados em tempo real, mantendo baixo acoplamento entre serviços e alta escalabilidade.


Implementação básica

Definindo os contratos: o papel das interfaces

Em qualquer arquitetura bem estruturada, as interfaces definem os contratos que orientam a comunicação entre os componentes. No contexto do Pub/Sub, dois contratos são fundamentais: o Subscriber e o PubSub.

Interface PubSub

package base;

import java.util.Collection;

public interface PubSub {
    void subscribe(String topic, Subscriber subscriber);
    void unsubscribe(String topic, Subscriber subscriber);
    void publish(String topic, Message message);
    Collection<String> getActiveTopics();
}

Interface Subscriber

package base;

public interface Subscriber {
    void onMessage(String topic, Object message);
}

A interface Subscriber representa qualquer componente capaz de receber mensagens. Ela define o método onMessage, que será invocado sempre que uma mensagem relevante for publicada em um tópico ao qual o subscriber está inscrito. Essa abstração garante que o sistema não precise conhecer os detalhes do consumidor, apenas que ele é capaz de processar uma mensagem recebida.

Por outro lado, a interface PubSub define o contrato do próprio intermediário. Ela estabelece os métodos responsáveis por gerenciar assinaturas, como subscribe e unsubscribe, e o método publish, responsável por distribuir mensagens. Além disso, expõe um mecanismo para consultar os tópicos ativos no sistema. Essa interface representa o núcleo da comunicação desacoplada.

Ao separar esses contratos das implementações, o sistema ganha flexibilidade e extensibilidade, permitindo a criação de diferentes tipos de publishers e subscribers sem alterar a infraestrutura base.


Encapsulando o conteúdo: a abstração da mensagem

A classe abstrata Message representa o conteúdo que circula pelo sistema. Sua função é encapsular os dados transmitidos entre publishers e subscribers. Ao introduzir essa abstração, o sistema deixa de depender de tipos específicos de mensagem. Em vez disso, qualquer tipo de conteúdo pode ser transportado, desde textos simples até objetos complexos, sem modificar a lógica do broker.

Essa abordagem segue o princípio de separação de responsabilidades. O broker é responsável pelo roteamento, enquanto a mensagem é responsável apenas por representar o conteúdo. Além disso, essa abstração permite especializações futuras, como mensagens específicas para e-mail, notificações, eventos de domínio ou integração entre serviços.

Classe abstrata Message

package base;

public abstract class Message {
    private final String content;

    public Message(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public String toString() {
        return String.format("Message content: %s", getContent());
    }
}

Implementando o broker: o coração do padrão Pub/Sub

A classe abstrata PubSubService representa a implementação central do broker. É nela que reside a lógica responsável por gerenciar tópicos e distribuir mensagens.

Internamente, o broker mantém uma estrutura de dados baseada em um mapa:

private final Map<String, List<Subscriber>> topics = new HashMap<>();

Esse mapa representa o modelo conceitual do Pub/Sub. Cada chave corresponde a um tópico, e cada valor representa a lista de subscribers interessados naquele tópico.

Quando um subscriber se inscreve, o método subscribe utiliza computeIfAbsent para garantir que o tópico exista antes de adicionar o novo subscriber. Essa abordagem simplifica a lógica e evita verificações manuais.

O método publish é responsável por realizar o desacoplamento efetivo. Em vez de conhecer diretamente os subscribers, o broker simplesmente consulta sua estrutura interna e encaminha a mensagem para todos os interessados. Cada subscriber recebe a notificação por meio da chamada ao método onMessage.

Nesse momento, ocorre o principal benefício do padrão: o publisher não conhece os subscribers, e os subscribers não conhecem o publisher. Ambos dependem apenas do broker.

Essa separação elimina dependências diretas e permite que novos componentes sejam adicionados sem impactar os existentes.

Classe abstrata PubSubService

package base;

import java.util.*;

public abstract class PubSubService implements PubSub {
    private final Map<String, List<Subscriber>> topics = new HashMap<>();
    private final String name;

    public PubSubService(String name) {
        this.name = name;
    }

    @Override
    public void subscribe(String topic, Subscriber subscriber) {
        topics.computeIfAbsent(topic, _ -> new ArrayList<>()).add(subscriber);
    }

    @Override
    public void unsubscribe(String topic, Subscriber subscriber) {
        if (Optional.ofNullable(topics.get(topic)).isPresent()) {
            this.topics.get(topic).remove(subscriber);
        }
    }

    @Override
    public void publish(String topic, Message message) {
        if (!topics.containsKey(topic)) {
            System.out.println("Topic " + topic + " does not exist.");
            return;
        }

        for (Subscriber sub : topics.get(topic)) {
            sub.onMessage(topic, message);
        }
    }

    @Override
    public Set<String> getActiveTopics() {
        return topics.keySet();
    }

    public String getName() { return name; }
}

Especialização do sistema: criando implementações concretasEspecialização do sistema: criando implementações concretas

Com a infraestrutura base estabelecida, a criação de implementações específicas torna-se simples e direta. A classe EmailMessage estende a abstração Message, representando uma mensagem específica para o domínio de e-mails. Sua responsabilidade é apenas especializar o comportamento da mensagem, sem interferir na lógica de roteamento. A classe EmailPublisher estende PubSubService, herdando toda a lógica do broker. Isso demonstra o poder das abstrações: o comportamento complexo já está implementado e pode ser reutilizado sem duplicação de código.

Já a classe EmailNotificationSubscriber representa um consumidor concreto. Ao implementar a interface Subscriber, ela define como o sistema deve reagir quando uma mensagem é recebida. Nesse caso, o comportamento consiste em exibir a mensagem no console, juntamente com o nome do subscriber e o tópico de origem.

Essas implementações demonstram claramente a separação entre infraestrutura e domínio, um dos princípios fundamentais de arquiteturas desacopladas.

Classe EmailMessage

package messages;

import base.Message;

public class EmailMessage extends Message {
    public EmailMessage(String content) {
        super(content);
    }

    @Override
    public String toString() {
        return String.format("Message content of [Email Message]: %s", getContent());
    }
}

Classe EmailPublisher

package publishers;

import base.PubSubService;

public class EmailPublisher extends PubSubService {
    public EmailPublisher() { super("Email Publisher"); }
}

Classe EmailNotificationSubscriber

package subscribers;

import base.Subscriber;

public class EmailNotificationSubscriber implements Subscriber {
    private final String name;

    public EmailNotificationSubscriber(String name) {
        this.name = name;
    }

    @Override
    public void onMessage(String topic, Object message) {
        System.out.println("[" + name + "] Received on " + topic + ": " + message.toString());
    }
}

O fluxo completo em execução

A classe principal PubSubEssential demonstra o funcionamento completo do sistema.

Inicialmente, o broker é criado por meio da instância de EmailPublisher. Esse objeto passa a ser responsável por gerenciar toda a comunicação entre os componentes.

Em seguida, são criados três subscribers independentes: Alice, Sarah e Joe. Cada um representa um consumidor interessado em diferentes tipos de mensagens.

Alice e Sarah se inscrevem no tópico PROMOTIONS, enquanto Joe se inscreve no tópico NOTIFICATIONS. Nesse momento, o broker registra essas associações internamente.

Quando uma mensagem é publicada no tópico PROMOTIONS, o broker identifica automaticamente os subscribers interessados e encaminha a mensagem apenas para Alice e Sarah. Da mesma forma, quando uma mensagem é publicada no tópico NOTIFICATIONS, apenas Joe recebe a notificação.

Esse fluxo demonstra claramente o desacoplamento proporcionado pelo padrão. O publisher não possui conhecimento sobre os subscribers, e os subscribers não possuem conhecimento sobre o publisher.

Toda a comunicação é mediada pelo broker.

Classe PubSub Essential

import base.Message;
import base.PubSub;
import base.Subscriber;
import messages.EmailMessage;
import publishers.EmailPublisher;
import subscribers.EmailNotificationSubscriber;

public class PubSubEssential {
    static void main(String[] args) {
        PubSub emailBroker = new EmailPublisher();

        Subscriber alice = new EmailNotificationSubscriber("Alice");
        Subscriber sarah = new EmailNotificationSubscriber("Sarah");
        Subscriber joe = new EmailNotificationSubscriber("Joe");

        emailBroker.subscribe("PROMOTIONS", alice);
        emailBroker.subscribe("PROMOTIONS", sarah);
        emailBroker.subscribe("NOTIFICATIONS", joe);

        Message promotionMessage = new EmailMessage("Here go on a promotion e-mail");
        Message notificationMessage = new EmailMessage("Here go on a notification e-mail");

        emailBroker.publish("PROMOTIONS", promotionMessage);
        emailBroker.publish("NOTIFICATIONS", notificationMessage);
    }
}

Considerações finais

Essa implementação demonstra, de forma clara e objetiva, como o padrão Publish–Subscribe pode ser construído utilizando princípios fundamentais de orientação a objetos, como abstração, encapsulamento e inversão de dependência.

Ao introduzir um intermediário responsável pela distribuição de mensagens, o sistema elimina dependências diretas entre componentes e torna-se mais flexível, extensível e escalável.

Esse modelo é amplamente utilizado em arquiteturas modernas, especialmente em sistemas distribuídos, microsserviços e plataformas orientadas a eventos. Compreender sua implementação em nível fundamental é essencial para entender como sistemas complexos conseguem manter baixo acoplamento enquanto crescem em escala e complexidade.

Navegação de Post

← Padrões Criacionais são seus melhores amigos no software

Deixe um comentário Cancelar resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Recent Posts

  • Publish–Subscribe em Java: como implementar o padrão PubSub passo a passo
  • Padrões Criacionais são seus melhores amigos no software
  • Microsserviços: 5 Lições Aprendidas — Um Olhar Arquitetural
  • 5 Pilares financeiros que destacam negócios de alto valor
  • Princípio da Responsabilidade Única e as implicações na Orientação a Objetos e Arquitetura de Software

Archives

  • fevereiro 2026
  • janeiro 2026
  • novembro 2025

Categories

  • Empreendimento (1)
  • Engenharia (11)
  • Finança (2)
  • Java (7)
  • Liderança (1)
  • Orientação a Objetos (10)
  • Programação (11)
  • Programação Estruturada (4)
  • Typescript (8)
© 2026 Reboredo.bit | Powered by Minimalist Blog WordPress Theme