Disclaimer
Este texto foi inicialmente concebido pela IA Generativa em função da transcrição de um vídeo do canal Dev + Eficiente. Se preferir acompanhar por vídeo, é só dar o play.
Introdução
O Outbox Namastech.io é um starter para Spring Boot que entrega, pronta para uso, a implementação do Outbox Pattern. Neste post, vamos analisar o código fonte desse projeto open source para entender como ele conseguiu implementar de maneira genérica o Outbox Pattern, de forma que você consiga adaptar para situações onde eventualmente precise aplicar esse padrão.
Por que Analisar Código de Projetos Open Source?
Esse não é o primeiro e nem vai ser o último post onde analiso códigos de projetos open source. Fazer esse exercício de analisar código alheio, principalmente de projetos open source, faz com que os códigos do dia a dia se tornem cada vez mais fáceis de entender.
Geralmente, em projetos open source você encontra código muito genérico, que precisa de técnicas de flexibilização. Quando você vai para o mundo corporativo, por mais que as regras de negócio eventualmente sejam até mais complexas, você tem situações que são mais diretas ao ponto. Então essa combinação de analisar código alheio — não só de projetos open source, mas de qualquer código que você tenha acesso — é um ótimo exercício para ganhar mais cancha, mais capacidade, tanto de analisar quanto de escrever código.
Entendendo o Outbox Pattern
O desenho do projeto resume bem a implementação do Outbox Pattern.
Você tem o código da aplicação, um componente seu que precisa salvar alguma coisa no banco de dados — uma nova entidade, por exemplo — e além disso precisa que uma outra operação seja realizada. Pode ser mandar um e-mail, operações geralmente de natureza mais assíncrona, operações onde você não tem muito controle de voltar atrás caso algo aconteça.
Imagine que você quer salvar um pedido e mandar um e-mail. Dá problema na hora de mandar o e-mail. E agora? Você quer tentar mandar o e-mail de novo, mas não quer salvar o pedido de novo.
O Outbox Pattern lida com esse tipo de situação assim: você salva a entidade e, ao invés de já realizar a ação secundária, você salva o evento que quer que aconteça também na mesma transação, numa tabela chamada Outbox Table. Você grava ali as informações necessárias para disparar aquele evento posteriormente.
Uma vez que você salvou, o Namastech.io tem um scheduler que faz o polling dos registros que foram salvos na tabela de Outbox. Ele puxa esse registro e processa. Geralmente o processador vai mandar a mensagem para um broker para que o que deveria acontecer, aconteça dentro do seu fluxo.
O ponto central é: você envolve a gravação da informação de negócio e os eventos relacionados àquela informação numa transação só. Se der problema, não aconteceu nada e você tenta fazer tudo de novo.
A Documentação e os Recursos da Biblioteca
A documentação está bem completa. O projeto tem suporte à garantia de entrega, um suporte interessante para observabilidade e monitoramento, e suporte para resiliência em relação ao envio dos eventos. Deu problema? Tenta de novo. Você pode configurar a política de retry que quiser, o tempo para fazer os novos retries. Tudo é configurável e o código é open source.
Como Usar a Biblioteca
No Getting Started, a documentação explica como configurar. Você precisa expor um componente que represente o relógio do sistema para ele poder controlar o timing e sequência das execuções. Ele fornece o script das tabelas que precisa para fazer a implementação. A versão padrão vem integrada com JPA.
Você precisa implementar a interface OutboxRecordProcessor. O método que a interface define é o process, que recebe um OutboxRecord. O OutboxRecord vai ter o tipo do evento, o ID de agregado e outras informações que você pode trabalhar.
Do ponto de vista do negócio, a documentação dá um exemplo: você recebe um comando de criação de novo pedido, salva (esse é o seu código de negócio) e cria um novo registro para ser gravado na tabela de Outbox.
O projeto oferece um Builder para facilitar a criação do registro. Você define o ID (ele chama de Aggregate ID), diz o tipo do evento, define um payload (ele sugere que você coloque um JSON), registra com o instante e salva. Ele já te dá um OutboxRepository para utilizar.
Analisando o Código Fonte
Vamos olhar um pouco do código fonte para ver como ele faz para chamar o seu processador e como mapeia o registro que vai ser salvo na Outbox Table. Spoiler: é código padrão, não tem nada demais, mas mesmo assim é legal compartilhar. É um exercício de entendimento de código alheio.
A classe OutboxRecord tem todas as informações. Em Kotlin, tudo que está com val na frente são variáveis de instância que são finais — você atribui um valor e não pode reatribuir. Você pode fazer relação com os métodos do Builder: o ID, o Aggregate ID, o Event Type e tudo mais.
class OutboxRecord<T> internal constructor(
val id: String,
val key: String,
val payload: T,
val partition: Int,
val createdAt: OffsetDateTime,
val handlerId: String,
status: OutboxRecordStatus,
completedAt: OffsetDateTime?,
failureCount: Int,
nextRetryAt: OffsetDateTime,
)
class Builder<T> {
private var key: String? = null
private var payload: T? = null
private var handlerId: String? = null
/**
* Sets the record key for the outbox record.
*
* @param key Identifier of the logical group of the record
* @return this Builder instance for method chaining
*/
fun key(key: String) = apply { this.key = key }
/**
* Sets the payload for the outbox record.
*
* @param payload Record payload
* @return this Builder instance for method chaining
*/
fun payload(payload: T) = apply { this.payload = payload }
fun handlerId(handlerId: String) = apply { this.handlerId = handlerId }
...
Tem também uma classe de autoconfiguração que é carregada automaticamente pelo Spring Boot, onde ele disponibiliza o OutboxProcessingScheduler. Esse scheduler recebe, entre outras coisas, o OutboxHandlerInvoker. Quem é o OutboxHandlerInvoker? É justamente alguém que invoca seu processador customizado de eventos, que implementa a interface OutboxHandler, do próprio projeto. Quem for usar essa biblioteca dentro do seu projeto vai ter uma implementação dessa interface.
No exemplo da documentação, você vai ter um processo de tomada de decisão: se o evento for esse, faz isso; se o evento for aquele, faz aquilo.
Como Funciona o Scheduler
O scheduler em si processa os eventos que foram gravados na Outbox Table para disparar posteriormente. Ele busca os Aggregate IDs, verifica se não está vazio, e para cada aggregate ele pega todos os registros relacionados, verifica se pode ser retentado agora, e processa.
Como ele processa? Ele recebe o OutboxHandlerInvoker e chama handler.invoke, passando os dados necessários para que posteriormente um objeto de uma classe que implementa a interface OutboxHandler possa ser carregado.
val metadata = OutboxRecordMetadata(record.key, record.handlerId, record.createdAt)
// Dispatch to appropriate handler based on handlerId
handlerInvoker.dispatch(record.payload, metadata)
Depois que processa, imagina que você mandou esse evento para um broker qualquer, a partir daí acabou o trabalho dele. Ele pega o registro, coloca uma data de completude e coloca um status dizendo que esse registro está finalizado.
Uma Biblioteca Simples
O que tem de complicado nessa biblioteca? É um código até mais padrão, um pattern muito conhecido, e ele tentou generalizar a aplicação desse pattern para que você possa colocar esse starter no seu projeto e automaticamente ter suporte a utilizar o Outbox Pattern nas situações que você entende que o projeto pede.
Por exemplo, no ContrateDevEficiente, a plataforma que tenta unir as pessoas que são alunas com o mercado de trabalho, estamos na primeira versão e tem algumas situações onde eu poderia ter usado o Outbox Pattern — quando tem esse lance de gravar e mandar um e-mail. Por enquanto estou correndo esse risco: gravei e deu problema na hora de mandar e-mail, então nesse momento fica sem mandar e-mail, depois lido com isso manualmente. Mas é o caso padrão de usar uma biblioteca como essa.
Uma coisa boa dessa lib, pelo menos na minha opinião, é que ela é simples. Por ser simples, você consegue entender exatamente o que está acontecendo, então você tem um pouco mais de confiança na hora de colocar algo assim dentro do seu projeto.
O que Eu Faria Diferente
O Problema do Acoplamento Mental
Quando pego o record e chamo markAsCompleted, pelo que entendo, um record só pode ser completado uma vez. Mas se olhar para esse código, ele não garante isso. Se eu chamar esse método três, quatro, cinco vezes, independente do ponto do código que eu chame, a data de completude vai mudando o tempo inteiro.
internal fun markCompleted(clock: Clock) {
if (status != OutboxRecordStatus.COMPLETED) {
completedAt = OffsetDateTime.now(clock)
status = OutboxRecordStatus.COMPLETED
}
}
Tem dois lugares que usam esse método: o scheduler e o teste. Quando a pessoa estava codando isso, provavelmente pensou: "Eu sei que o markCompleted só é chamado nesse lugar, então não vai acontecer de alguém tentar marcar como finalizado um record que já foi finalizado".
Mas do ponto de vista desse código, esse método não sabe quem vai chamar ele. Pensando em camada, esse método está na camada de baixo. Quem está na camada de baixo nunca sabe quem vai chamar(pelo menos não deveria).
Quando quem está na camada de baixo assume o comportamento da camada de cima e não verifica se esse comportamento foi realmente respeitado, é o que eu chamo de acoplamento mental. Existe um acoplamento implícito: o markCompleted sabe que só vai ser chamado dali e ali só é executado uma vez, logo não precisa verificar.
Se eu tivesse codando, eu faria uma verificação. Algo como: o status precisa ser diferente de completed para eu poder completar. E provavelmente também não consigo marcar um record como failed se ele está completed. Esse é o tipo de código que eu faço regularmente.
Comportamento Perto de Estado
Por outro lado, tem uma coisa que eu particularmente gosto bastante quando olho para o código: é um código que realmente tenta colocar comportamento perto de estado.
Comportamento perto de estado maximiza coesão e é algo que linguagens que suportam orientação a objeto facilitam por padrão. Na minha opinião, facilita escrever teste automatizado na granularidade certa. Você também ganha reuso de métodos de graça, sem nenhum esforço adicional.
Processadores Especializados
No Builder que foi construído, nesse momento o projeto define uma interface e você tem uma implementação dessa interface que vai lidar com todos os eventos. É um jeito de fazer.
Um outro jeito de fazer seria, no Builder, poder passar o processador que lida especificamente com aquele evento. Eu passaria, ele gravaria o nome completo dessa classe na tabela, associado lá na tabela dos eventos que tem que ser tratados. No job, na hora de pegar um record para tratar, eu pegaria essa classe, utilizaria o container do Spring para carregar a implementação e chamaria o mesmo método da interface. Não mudaria nada na interface, só que aí eu teria processadores especializados para aquele determinado tipo de evento.
Gosto dessa ideia porque mantenho o processador mais enxuto e facilito a criação de teste de unidade para o meu processador específico, caso eu queira. Eventualmente pode ser uma tentativa de pull request para esse projeto.
Na última versão do código já é suportado processadores super especializados.
Conclusão
Esse é o fluxo do Outbox Namastech.io. Um código padrão, um pattern muito conhecido, mas mesmo assim é legal compartilhar e fazer esse exercício de análise de código alheio. Alguém publicou sobre esse projeto no LinkedIn, depois Rafael Ponte mandou no Discord do Dev + Eficiente. Eu sempre falo para a galera da comunidade que um exercício legal é analisar o código fonte de outros projetos. E aqui estou eu fazendo o mesmo exercício que recomendo.
Dev+ Eficiente
Este conteúdo é parte do ecossistema Dev+ Eficiente, mantido por Alberto junto com Maurício Aniche e Rafael Ponte, que inclui um canal e dois treinamentos. O primeiro é a Jornada Dev+ Eficiente, cujo foco é fazer com que você seja capaz de entregar software que de fato gera valor com o máximo de qualidade e eficiência.
O segundo é a especialização em Engenharia de IA, uma parceria com Daniel Romero, cuja ideia é habilitar você para entregar software de excelência, integrando sistemas com LLMs.
Conheça mais em https://deveficiente.com/interesse-especializacao-engenharia-ia

Top comments (0)