Aula 07 · 50–70 minutos
Domain Events: nomeando uma decisão que Executar já tomava escondida
Aula 7 — por que uma decisão de negócio desaparecia ao virar só um novo valor de Status; o padrão Domain Event de Martin Fowler, os tipos PedidoAprovado e PedidoRejeitado, e main.go voltando a ser quem observa o que o domínio produziu.
Resultado e mapa da aula
Desde a Lição 6, Pedido sabe representar três estados — pendente, aprovado, rejeitado — sem estados impossíveis. Mas há um problema mais sutil escondido ali: quando AprovarAutomaticamente() ou Rejeitar() mudam o Status, essa mudança é só isso — um novo valor sobrescrevendo o antigo. Nada no sistema registra, como um fato observável, que uma decisão acabou de ser tomada. Esta aula nomeia esse fato — o Domain Event — e mostra onde ele nasce, quem o repassa, e quem observa.
- O problema: uma decisão de negócio que acontece e é imediatamente esquecida.
- Fonte primária: o padrão Domain Event, de Martin Fowler.
- Uma distinção que evita confusão de mercado: Domain Event não é Event Sourcing.
- Uma abordagem plausível, mas incorreta: um evento genérico, com um campo
Tipoe dados soltos — o mesmo anti-padrão das Lições 5 e 6, de novo. - A correção: tipos nomeados, um por fato de negócio —
PedidoAprovado,PedidoRejeitado. - Onde o evento nasce (domínio), quem repassa (use case) e quem observa (main.go) — o Composition Root da Lição 4 reaparece, e um mecanismo de Go da Lição 6 volta a aparecer, sem aviso.
- O que falta, de propósito: hoje o evento só é impresso; a persistência real, na mesma transação do
Pedido, é assunto da Lição 12.
O problema: uma decisão tomada e imediatamente esquecida
Releia o Executar de AprovarPedido, como ficou depois da Lição 6:
// internal/usecase/aprovar_pedido.go — como está hoje
func (uc *AprovarPedido) Executar(p domain.Pedido) error {
p.AprovarAutomaticamente()
return uc.repo.Salvar(p)
}
Se p.Total está dentro do limite, p.Status muda de StatusPendente para StatusAprovado, e essa versão nova é salva. Do ponto de vista de quem chama Executar, algo significativo para o negócio acabou de acontecer: um pedido foi aprovado. Mas nada no código representa esse acontecimento como um fato — ele só existe, de forma implícita, como a diferença entre o Status antigo e o novo, e essa diferença desaparece no instante em que Salvar sobrescreve o registro. Se amanhã alguma parte do sistema precisar saber "quando" ou "que" pedidos foram aprovados — uma notificação, um relatório, um log de auditoria — não há nada para consultar: só o estado atual, sem histórico do que levou até ali.
Fonte primária: o padrão Domain Event, de Martin Fowler
Repare no que essa fonte não exige: publicar em uma fila, notificar outros serviços, ou qualquer forma de mensageria distribuída — isso está fora do escopo desta trilha, como já registrado na missão do curso. O que o padrão exige é mais simples e mais fundamental: nomear o fato como um tipo de dado, para que ele possa existir, ser passado adiante e ser observado — antes de decidir o que fazer com ele.
Uma distinção que evita confusão de mercado: Domain Event não é Event Sourcing
É comum, ao ouvir "Domain Event" pela primeira vez, associar o termo imediatamente a Event Sourcing — um estilo de arquitetura mais radical, em que o histórico de eventos é a fonte da verdade do sistema, e o estado atual (como o Status de um Pedido) é sempre recalculado a partir desse histórico, nunca guardado diretamente. O próprio Fowler, na mesma página citada acima, esclarece a relação entre os dois:
Isso importa para localizar exatamente o que este projeto faz, e o que não faz. memoria.PedidoRepository.Salvar continua gravando p.Status diretamente — o valor atual, não uma sequência de eventos que precisaria ser reprocessada para descobrir o status. Se pedido_eventos (a tabela que a Lição 12 introduz) desaparecesse amanhã, o sistema ainda saberia, com certeza, se cada pedido está aprovado ou rejeitado, porque essa informação vive em Status, não só no histórico de eventos. Isso é Domain Event sem Event Sourcing: o evento existe como um registro adicional do que aconteceu, útil para auditoria, notificação e histórico — mas o estado atual continua sendo gravado e lido diretamente, do jeito mais simples possível. Adotar Event Sourcing de verdade — reconstruir Status a partir de uma sequência de PedidoAprovado/PedidoRejeitado em vez de lê-lo de uma coluna — seria uma mudança de arquitetura muito mais profunda, fora do escopo desta trilha, e desnecessária para o problema que o projeto resolve.
Uma abordagem plausível, mas incorreta: um evento genérico
Por que essa abordagem parece razoável: um único tipo EventoPedido cobriria qualquer evento futuro sem precisar declarar um novo tipo Go a cada vez — Tipo: "aprovado" para um caso, Tipo: "rejeitado" para outro, e Dados guardando o que for preciso em cada situação.
Por que falha: este é o mesmo anti-padrão que já apareceu duas vezes nesta trilha, sob outra roupagem. A Lição 5 mostrou que um pacote models genérico, identificado só por convenção e não pelo compilador, grita menos sobre o negócio do que tipos nomeados — aqui, Tipo: "aprovado" é uma string solta que só significa algo se todo o código concordar, por convenção, com a grafia exata; um typo ("aprvado") compila normalmente e falha silenciosamente em runtime. A Lição 6 mostrou que campos soltos permitem estados que não deveriam existir — aqui, nada impede construir um EventoPedido de aprovação com Dados vazio, sem o valor total aprovado, ou com Tipo grafado errado, um "tipo" de evento que não existe em lugar nenhum do sistema. map[string]any piora isso: o compilador não verifica se as chaves esperadas existem, nem se os valores têm o tipo certo — um erro que só aparece em runtime, tarde demais, exatamente o tipo de garantia que os dois tipos concretos da Lição 6 (StatusAprovado, StatusRejeitado) eliminaram para o status do pedido.
A correção: um tipo por fato de negócio
A correção é a mesma lição das duas aulas anteriores, aplicada a um problema novo: nomear cada fato com seu próprio tipo, contendo exatamente os campos que aquele fato precisa — nem mais, nem menos.
// internal/domain/eventos.go
package domain
import "time"
type PedidoAprovado struct {
PedidoID string
Total float64
Ocorrido time.Time
}
type PedidoRejeitado struct {
PedidoID string
Ocorrido time.Time
}
PedidoAprovado carrega Total porque esse dado importa especificamente para um pedido aprovado (por exemplo, para somar valores aprovados num relatório); PedidoRejeitado não carrega, porque não há uso de negócio para esse dado num evento de rejeição — a ausência do campo é uma decisão de design, não um esquecimento. Comparando com a versão genérica: não existe like construir um PedidoAprovado sem Total, porque o compilador exige o tipo completo; não existe erro de digitação no "tipo" do evento, porque o tipo do evento é o próprio tipo Go, verificado em tempo de compilação.
Onde o evento nasce: dentro do domínio, não do use case
O evento é produzido no exato lugar onde a decisão acontece — dentro dos métodos de Pedido que já mudam Status, não em uma camada externa que só observa o resultado:
// internal/domain/pedido.go — métodos atualizados
func (p *Pedido) AprovarAutomaticamente() *PedidoAprovado {
if p.Total > limiteAprovacaoAutomatica {
return nil
}
p.Status = StatusAprovado
return &PedidoAprovado{PedidoID: p.ID, Total: p.Total, Ocorrido: time.Now()}
}
func (p *Pedido) Rejeitar() *PedidoRejeitado {
p.Status = StatusRejeitado
return &PedidoRejeitado{PedidoID: p.ID, Ocorrido: time.Now()}
}
Isso segue a Regra de Dependência da Lição 1 pela mesma razão que StatusPedido seguiu na Lição 6: o evento é um conceito de negócio — "um pedido foi aprovado" — não um detalhe técnico de como ou onde esse fato será usado depois. Declará-lo dentro de domain, no mesmo pacote que decide a regra de aprovação, garante que o evento só possa ser criado quando a decisão de negócio correspondente realmente aconteceu — não é possível construir um PedidoAprovado de fora, sem passar pela regra que o justifica, porque o próprio método AprovarAutomaticamente é o único lugar do sistema que produz esse valor.
O use case não cria o evento — ele só o repassa:
// internal/usecase/aprovar_pedido.go
func (uc *AprovarPedido) Executar(p domain.Pedido) (*domain.PedidoAprovado, error) {
evento := p.AprovarAutomaticamente()
if err := uc.repo.Salvar(p); err != nil {
return nil, err
}
return evento, nil
}
// internal/usecase/rejeitar_pedido.go
func (uc *RejeitarPedido) Executar(p domain.Pedido) (*domain.PedidoRejeitado, error) {
evento := p.Rejeitar()
if err := uc.repo.Salvar(p); err != nil {
return nil, err
}
return evento, nil
}
E main.go, o Composition Root já apresentado na Lição 4, é quem primeiro tem a chance de observar o evento e decidir o que fazer com ele:
// main.go
pedido := domain.Pedido{ID: "1", Total: 750}
evento, err := aprovarPedido.Executar(pedido)
if err != nil {
fmt.Println("erro:", err)
return
}
if evento != nil {
fmt.Printf("evento: pedido %s aprovado às %s\n", evento.PedidoID, evento.Ocorrido.Format(time.RFC3339))
}
O evento nasce dentro de domain, no método que decide a regra de negócio. usecase não cria nada — só repassa o que domain produziu. main.go, o Composition Root, é a primeira camada que observa o evento e decide o que fazer com ele. O adapter memoria nunca sabe que o evento existe — ele só recebe e salva o Pedido.
Note que memoria.PedidoRepository.Salvar não muda nesta aula — ele continua recebendo só um domain.Pedido, sem qualquer conhecimento do evento. Isso é deliberado: hoje, main.go apenas imprime o evento no terminal, um placeholder propositalmente simples. A Lição 12 — quando o projeto ganhar um banco de dados real (Lição 11) e precisar decidir como salvar o Pedido e um registro do evento como uma única operação atômica — é onde esse evento passa a ser persistido de verdade, na tabela pedido_eventos, e onde a pergunta "por que essa publicação precisa ser síncrona?" tem uma resposta concreta, ligada à garantia transacional. Por enquanto, a única responsabilidade desta aula é garantir que o fato exista como um valor nomeado, disponível para quem precisar dele — a infraestrutura para persisti-lo chega quando a infraestrutura de persistência, ela mesma, chegar.
Prática guiada: nomeie o terceiro fato que falta
Raciocínio antes do código
A parte (1) e (2) são diretas, seguindo o padrão já estabelecido nesta aula. A parte (3) é onde a dificuldade real está: AprovarAutomaticamente agora pode produzir um de dois tipos diferentes de evento — não "um evento ou nada", como hoje, mas "um tipo de evento ou outro". Go não tem um tipo de união (como PedidoAprovado | PedidoRequerRevisaoManual em TypeScript) — é preciso escolher como representar "um valor que pode ser um destes tipos específicos" na assinatura de uma função.
Uma abordagem plausível, mas incorreta: dois retornos opcionais paralelos
// internal/usecase/aprovar_pedido.go — abordagem incorreta
func (uc *AprovarPedido) Executar(p domain.Pedido) (*domain.PedidoAprovado, *domain.PedidoRequerRevisaoManual, error) {
// ...
}
Por que falha: esse desenho é o mesmo erro dos dois bools independentes da Lição 6, só que em forma de retorno de função em vez de campos de struct. Nada impede, na assinatura, que os dois primeiros valores retornados sejam não-nulos ao mesmo tempo — uma situação sem sentido de negócio, que o chamador (main.go) precisaria adivinhar como tratar. E o inverso também é possível: os dois retornarem nil simultaneamente, silenciosamente perdendo o fato que deveria ter acontecido. Cada if extra necessário em main.go para lidar com essas combinações é a mesma dívida de manutenção que motivou a correção da Lição 6 — só que reintroduzida aqui, uma aula depois, por um caminho diferente.
A solução correta: uma interface fechada para representar "um dos dois"
// internal/domain/eventos.go — interface fechada
package domain
type EventoAprovacao interface {
eventoAprovacao()
}
func (PedidoAprovado) eventoAprovacao() {}
func (PedidoRequerRevisaoManual) eventoAprovacao() {}
type PedidoRequerRevisaoManual struct {
PedidoID string
Ocorrido time.Time
}
// internal/domain/pedido.go — nunca mais retorna nil
func (p *Pedido) AprovarAutomaticamente() EventoAprovacao {
if p.Total > limiteAprovacaoAutomatica {
return PedidoRequerRevisaoManual{PedidoID: p.ID, Ocorrido: time.Now()}
}
p.Status = StatusAprovado
return PedidoAprovado{PedidoID: p.ID, Total: p.Total, Ocorrido: time.Now()}
}
// internal/usecase/aprovar_pedido.go
func (uc *AprovarPedido) Executar(p domain.Pedido) (domain.EventoAprovacao, error) {
evento := p.AprovarAutomaticamente()
if err := uc.repo.Salvar(p); err != nil {
return nil, err
}
return evento, nil
}
// main.go — distinguindo os dois eventos com um type switch
evento, err := aprovarPedido.Executar(pedido)
if err != nil {
fmt.Println("erro:", err)
return
}
switch e := evento.(type) {
case domain.PedidoAprovado:
fmt.Printf("aprovado: %s, total %.2f\n", e.PedidoID, e.Total)
case domain.PedidoRequerRevisaoManual:
fmt.Printf("requer revisão manual: %s\n", e.PedidoID)
}
Por que está correto: EventoAprovacao é o mesmo padrão que a satisfação implícita da Lição 2 já ensinou, aplicado com um propósito diferente — o método eventoAprovacao() não é minúsculo por acaso; começar com letra minúscula o torna não exportado, o que significa que só tipos declarados dentro do próprio pacote domain conseguem implementá-lo. Isso fecha o conjunto de tipos possíveis: quem lê a assinatura Executar(p domain.Pedido) (domain.EventoAprovacao, error) sabe, com certeza, que o valor retornado só pode ser um dos tipos que domain decidiu — nunca um terceiro tipo inventado em outro pacote. Esse é o mesmo padrão usado na biblioteca padrão do Go: o pacote go/ast declara uma interface Expr com um método não exportado, implementada só por tipos como *ast.Ident e *ast.BasicLit, do próprio pacote — garantindo que nenhum código externo possa "inventar" um novo tipo de expressão de sintaxe que o resto do compilador não saiba tratar.
Verificação: o switch e := evento.(type) em main.go é um type switch — verifica, em tempo de execução, qual tipo concreto está guardado dentro da interface EventoAprovacao. Escreva um teste para AprovarAutomaticamente que confirma os dois cenários, usando uma asserção de tipo (_, ok := evento.(domain.PedidoAprovado)) para verificar qual tipo concreto voltou:
// internal/domain/pedido_test.go
func TestAprovarAutomaticamente_RetornaPedidoAprovado_DentroDoLimite(t *testing.T) {
p := &Pedido{ID: "1", Total: 500}
evento := p.AprovarAutomaticamente()
aprovado, ok := evento.(PedidoAprovado)
if !ok {
t.Fatalf("esperava PedidoAprovado, obteve %T", evento)
}
if aprovado.PedidoID != "1" {
t.Errorf("esperava PedidoID 1, obteve %s", aprovado.PedidoID)
}
}
func TestAprovarAutomaticamente_RetornaRequerRevisaoManual_AcimaDoLimite(t *testing.T) {
p := &Pedido{ID: "2", Total: 5000}
evento := p.AprovarAutomaticamente()
if _, ok := evento.(PedidoRequerRevisaoManual); !ok {
t.Fatalf("esperava PedidoRequerRevisaoManual, obteve %T", evento)
}
if p.Status != StatusPendente {
t.Errorf("esperava StatusPendente sem alteração, obteve %v", p.Status)
}
}
O segundo teste confirma duas coisas ao mesmo tempo: o tipo concreto certo voltou, e p.Status permanece StatusPendente — a prova de que, agora, existe sempre um evento explícito para esse caminho, em vez do silêncio de um nil. Rode go build ./... para confirmar que main.go trata os dois casos do switch sem erro de compilação, e go test ./internal/domain/... para confirmar os dois cenários.
Perguntas de entrevista e de mercado
"O que é um Domain Event, e por que ele não é a mesma coisa que uma mensagem numa fila (RabbitMQ, Kafka)?" Separe o conceito da infraestrutura: um Domain Event, segundo Fowler, é a representação de um fato de negócio como um objeto de primeira classe — isso existe independentemente de como (ou se) esse fato é transmitido para outro sistema. Filas, tópicos e brokers são uma forma possível de publicar um evento depois que ele existe; o padrão em si não exige nenhuma dessas ferramentas. Cite este projeto como exemplo: o evento existe e é observado em main.go, sem fila nenhuma.
"Por que o evento é criado dentro de domain.Pedido, e não dentro do use case ou do handler HTTP?" Aponte a mesma lógica da Regra de Dependência: o evento representa um fato de negócio, e só o código que decide a regra de negócio (AprovarAutomaticamente) tem informação suficiente para saber que o fato realmente aconteceu. Se o evento fosse criado no use case ou mais externamente, existiria o risco de criar um PedidoAprovado sem que a regra de aprovação tivesse de fato sido satisfeita — um evento "mentiroso", desconectado da decisão real.
"Este projeto ainda não publica eventos para lugar nenhum — só imprime no main.go. Isso não é um Domain Event incompleto, ou até inútil?" Não — é uma aplicação deliberadamente incremental do padrão. Nomear o fato como um tipo é o primeiro passo, valioso por si só (torna o código mais expressivo, mais testável, e prepara o terreno). O "para onde esse evento vai" é uma decisão de infraestrutura, resolvida à medida que a infraestrutura existe — aqui, quando a Lição 12 introduzir persistência transacional. Construir um sistema de publicação antes de haver qualquer consumidor real seria complexidade especulativa, o oposto do que a trilha pratica.
"Como você explicaria, numa entrevista, por que EventoAprovacao usa um método não exportado (eventoAprovacao()) em vez de simplesmente declarar dois métodos de use case separados?" Explique que a alternativa — dois métodos de use case, cada um retornando só um tipo concreto — funcionaria, mas obrigaria o chamador a já saber, antecipadamente, qual dos dois vai acontecer, o que é justamente o que não se sabe até AprovarAutomaticamente decidir. A interface fechada permite que Executar tenha uma única assinatura, retornando "um dos eventos válidos", deixando para o chamador (via type switch) decidir o que fazer com cada caso — sem abrir mão da garantia de que só tipos do pacote domain podem aparecer ali.
"Qual é a diferença entre Domain Event e Event Sourcing? Este projeto usa os dois, ou só um deles?" Uma resposta forte não trata os dois como sinônimos. Cite a própria fonte: Fowler descreve Domain Event como pré-requisito de Event Sourcing, não como o mesmo padrão — Event Sourcing exige que todas as escritas do sistema aconteçam através de eventos, com o estado atual sendo derivado do histórico; Domain Event, sozinho, só exige nomear o fato como um valor. Este projeto usa Domain Event sem Event Sourcing: Status continua sendo lido e escrito diretamente em memoria.PedidoRepository, e o evento é um registro adicional, não a fonte da verdade. Reconhecer essa distinção evita o erro comum de achar que "ter um tipo PedidoAprovado" já significa "este sistema faz Event Sourcing".
Recupere da memória: 10 questões
Recupere da memória: 10 questões
Segundo a definição de Martin Fowler citada nesta aula, o que um Domain Event captura?
Antes desta aula, o que acontecia com o fato 'um pedido foi aprovado' depois que AprovarPedido.Executar rodava?
Por que EventoPedido com um campo Tipo string e Dados map[string]any é uma abordagem incorreta para representar eventos?
Por que PedidoAprovado carrega o campo Total, mas PedidoRejeitado não?
Em qual pacote o evento PedidoAprovado é efetivamente criado (onde o valor é construído pela primeira vez)?
Por que o evento nasce dentro de domain, e não dentro do use case ou de um handler HTTP mais externo?
Nesta aula, o que main.go faz com o evento retornado por AprovarPedido.Executar?
O adapter memoria.PedidoRepository muda nesta aula para também salvar o evento?
No exercício desta aula, por que retornar (*PedidoAprovado, *PedidoRequerRevisaoManual, error) de Executar é uma abordagem incorreta?
Na solução correta do exercício, por que o método eventoAprovacao() é declarado com letra minúscula (não exportado)?
Segundo a citação de Fowler sobre Event Sourcing, qual é a relação entre Domain Event e Event Sourcing?
Este projeto usa Event Sourcing?
Em Executar(p domain.Pedido), p é recebido por valor. Por que p.AprovarAutomaticamente() — um método com pointer receiver — ainda assim consegue alterar p.Status com sucesso dentro de Executar?
Feche o modelo mental
Uma decisão de negócio que só muda um campo de estado desaparece no instante em que é sobrescrita — nada fica para trás como um fato consultável. Um Domain Event, na definição de Fowler, é a correção: representar esse fato como um valor nomeado de primeira classe. Isso não é Event Sourcing — Status continua sendo a fonte da verdade, lida e escrita diretamente; o evento é um registro adicional, não o único mecanismo de escrita do sistema. A tentação de um evento genérico (Tipo string, Dados map[string]any) repete o mesmo erro das Lições 5 e 6 — dados soltos que o compilador não consegue verificar. A correção é a mesma das duas aulas anteriores: um tipo por fato, com exatamente os campos que aquele fato precisa. O evento nasce dentro de domain, onde a decisão de negócio realmente acontece; o use case só repassa; main.go, o Composition Root, é quem primeiro observa. Nada disso exige mensageria, fila ou publicação distribuída — só nomear o que já estava acontecendo escondido. A persistência de verdade, na mesma transação que salva o Pedido, é assunto para quando a infraestrutura de banco de dados, ela mesma, existir.
Próxima aula planejada: Um segundo use case — consultar um pedido já decidido, e o que muda quando o repositório precisa responder, não só guardar.
Referências primárias para aprofundar
O padrão central desta aula é Domain Event, Martin Fowler. A técnica de interface fechada usada na solução do exercício é documentada, como aplicada na prática, em go/ast — Expr, biblioteca padrão do Go, e segue o mesmo princípio de satisfação implícita já citado na Lição 2 a partir de The Go Programming Language Specification — Interface types.
Consulte também o glossário desta trilha.