Domain-Driven Design — introduzido por Eric Evans no livro homônimo de 2003 — é uma abordagem de desenvolvimento onde o modelo de domínio do negócio guia todas as decisões de design. Não é uma arquitetura técnica, mas uma filosofia: o código deve falar a linguagem dos especialistas do negócio, e a estrutura do sistema deve refletir a estrutura do problema que ele resolve.
DDD e Clean Architecture se complementam naturalmente. Clean Architecture define onde colocar as peças. DDD define como modelar essas peças de forma que reflitam o negócio com precisão e clareza.
Os blocos de construção do DDD
Ubiquitous Language: a linguagem compartilhada
O primeiro princípio do DDD é criar uma linguagem compartilhada entre desenvolvedores e especialistas do negócio. Essa linguagem deve aparecer no código — nomes de tipos, métodos, pacotes e variáveis devem refletir os termos que o negócio usa.
// Ruim: terminologia técnica
type Record struct {
UserID int
ItemID int
Qty int
UnitCost float64
}
func (r *Record) Calc() float64 { return float64(r.Qty) * r.UnitCost }
// Bom: terminologia do negócio (e-commerce)
type ItemPedido struct {
ProdutoID int64
Quantidade int
PrecoUnit float64
}
func (i *ItemPedido) Subtotal() float64 {
return float64(i.Quantidade) * i.PrecoUnit
}
Entidades: identidade ao longo do tempo
Uma entidade é um objeto com identidade única que persiste ao longo do tempo. Dois objetos com o mesmo ID são a mesma entidade, independentemente de seus outros atributos.
// internal/domain/cliente/cliente.go
package cliente
import (
"errors"
"regexp"
"time"
)
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
var (
ErrEmailInvalido = errors.New("email inválido")
ErrNomeObrigatorio = errors.New("nome é obrigatório")
ErrClienteInativo = errors.New("cliente está inativo")
)
// ClienteID é o identificador único de um cliente.
// Usar um tipo nomeado evita confusão com IDs de outros agregados.
type ClienteID int64
// Cliente é uma entidade — sua identidade é o ClienteID.
type Cliente struct {
id ClienteID
nome string
email string
telefone string
ativo bool
vip bool
criadoEm time.Time
eventos []Evento // eventos de domínio pendentes
}
type Evento interface {
NomeEvento() string
}
type ClienteCriado struct {
ClienteID ClienteID
Email string
OcorridoEm time.Time
}
func (e ClienteCriado) NomeEvento() string { return "cliente.criado" }
type EmailAlterado struct {
ClienteID ClienteID
EmailAnterior string
EmailNovo string
OcorridoEm time.Time
}
func (e EmailAlterado) NomeEvento() string { return "cliente.email_alterado" }
// NovoCliente cria um Cliente validado.
func NovoCliente(nome, email, telefone string) (*Cliente, error) {
if nome == "" {
return nil, ErrNomeObrigatorio
}
if !emailRegex.MatchString(email) {
return nil, ErrEmailInvalido
}
c := &Cliente{
nome: nome,
email: email,
telefone: telefone,
ativo: true,
criadoEm: time.Now().UTC(),
}
c.registrarEvento(ClienteCriado{
Email: email,
OcorridoEm: time.Now().UTC(),
})
return c, nil
}
// ReconstituirCliente cria uma entidade a partir de dados persistidos.
// Não dispara eventos — a entidade já existia.
func ReconstituirCliente(id ClienteID, nome, email, telefone string, ativo, vip bool, criadoEm time.Time) *Cliente {
return &Cliente{
id: id,
nome: nome,
email: email,
telefone: telefone,
ativo: ativo,
vip: vip,
criadoEm: criadoEm,
}
}
func (c *Cliente) ID() ClienteID { return c.id }
func (c *Cliente) Nome() string { return c.nome }
func (c *Cliente) Email() string { return c.email }
func (c *Cliente) Telefone() string { return c.telefone }
func (c *Cliente) Ativo() bool { return c.ativo }
func (c *Cliente) EhVIP() bool { return c.vip }
func (c *Cliente) CriadoEm() time.Time { return c.criadoEm }
func (c *Cliente) AlterarEmail(novoEmail string) error {
if !emailRegex.MatchString(novoEmail) {
return ErrEmailInvalido
}
if novoEmail == c.email {
return nil
}
emailAnterior := c.email
c.email = novoEmail
c.registrarEvento(EmailAlterado{
ClienteID: c.id,
EmailAnterior: emailAnterior,
EmailNovo: novoEmail,
OcorridoEm: time.Now().UTC(),
})
return nil
}
// TornarVIP promove o cliente. Só clientes ativos podem ser promovidos.
func (c *Cliente) TornarVIP() error {
if !c.ativo {
return ErrClienteInativo
}
c.vip = true
return nil
}
func (c *Cliente) Desativar() error {
if !c.ativo {
return ErrClienteInativo
}
c.ativo = false
return nil
}
// Eventos retorna os eventos de domínio acumulados e limpa a fila.
func (c *Cliente) Eventos() []Evento {
eventos := c.eventos
c.eventos = nil
return eventos
}
func (c *Cliente) registrarEvento(e Evento) {
c.eventos = append(c.eventos, e)
}
Value Objects: igualdade por valor
Value Objects não têm identidade — dois Value Objects com os mesmos atributos são equivalentes. São imutáveis: qualquer "mudança" cria um novo objeto.
// internal/domain/shared/dinheiro.go
package shared
import (
"errors"
"fmt"
)
var (
ErrMoedaDiferente = errors.New("operação entre moedas diferentes")
ErrValorNegativo = errors.New("valor monetário não pode ser negativo")
)
// Dinheiro é um Value Object que representa um valor monetário.
// É imutável — operações retornam um novo Dinheiro.
type Dinheiro struct {
valor int64 // centavos — evita problemas de float
moeda string // ISO 4217: BRL, USD, EUR
}
func NovoDinheiro(valor int64, moeda string) (Dinheiro, error) {
if valor < 0 {
return Dinheiro{}, ErrValorNegativo
}
if moeda == "" {
moeda = "BRL"
}
return Dinheiro{valor: valor, moeda: moeda}, nil
}
func DinheiroEmReais(centavos int64) Dinheiro {
d, _ := NovoDinheiro(centavos, "BRL")
return d
}
func (d Dinheiro) Valor() int64 { return d.valor }
func (d Dinheiro) Moeda() string { return d.moeda }
func (d Dinheiro) EmReais() float64 { return float64(d.valor) / 100 }
func (d Dinheiro) Adicionar(outro Dinheiro) (Dinheiro, error) {
if d.moeda != outro.moeda {
return Dinheiro{}, ErrMoedaDiferente
}
return Dinheiro{valor: d.valor + outro.valor, moeda: d.moeda}, nil
}
func (d Dinheiro) Subtrair(outro Dinheiro) (Dinheiro, error) {
if d.moeda != outro.moeda {
return Dinheiro{}, ErrMoedaDiferente
}
if d.valor < outro.valor {
return Dinheiro{}, ErrValorNegativo
}
return Dinheiro{valor: d.valor - outro.valor, moeda: d.moeda}, nil
}
func (d Dinheiro) Multiplicar(fator int64) Dinheiro {
return Dinheiro{valor: d.valor * fator, moeda: d.moeda}
}
func (d Dinheiro) IgualA(outro Dinheiro) bool {
return d.valor == outro.valor && d.moeda == outro.moeda
}
func (d Dinheiro) String() string {
return fmt.Sprintf("%s %.2f", d.moeda, d.EmReais())
}
// CPF é um Value Object para CPF brasileiro.
type CPF struct {
valor string
}
func NovoCPF(valor string) (CPF, error) {
limpo := limparCPF(valor)
if !validarCPF(limpo) {
return CPF{}, errors.New("CPF inválido")
}
return CPF{valor: limpo}, nil
}
func (c CPF) String() string {
if len(c.valor) != 11 {
return c.valor
}
return fmt.Sprintf("%s.%s.%s-%s",
c.valor[0:3], c.valor[3:6],
c.valor[6:9], c.valor[9:11])
}
func (c CPF) Valor() string { return c.valor }
func (c CPF) IgualA(outro CPF) bool { return c.valor == outro.valor }
func limparCPF(cpf string) string {
resultado := ""
for _, r := range cpf {
if r >= '0' && r <= '9' {
resultado += string(r)
}
}
return resultado
}
func validarCPF(cpf string) bool {
if len(cpf) != 11 {
return false
}
// Dígitos todos iguais (000.000.000-00, 111.111.111-11...) passam no
// cálculo abaixo, mas não são CPFs válidos
iguais := true
for i := 1; i < 11; i++ {
if cpf[i] != cpf[0] {
iguais = false
break
}
}
if iguais {
return false
}
// Verificação dos dígitos verificadores
soma := 0
for i := 0; i < 9; i++ {
soma += int(cpf[i]-'0') * (10 - i)
}
resto := (soma * 10) % 11
if resto == 10 || resto == 11 {
resto = 0
}
if resto != int(cpf[9]-'0') {
return false
}
soma = 0
for i := 0; i < 10; i++ {
soma += int(cpf[i]-'0') * (11 - i)
}
resto = (soma * 10) % 11
if resto == 10 || resto == 11 {
resto = 0
}
return resto == int(cpf[10]-'0')
}
Agregados: consistência transacional
Um Agregado é um cluster de entidades e Value Objects tratados como uma unidade para fins de mudança de dados. O Aggregate Root é a única porta de entrada para modificações.
// internal/domain/pedido/pedido.go
package pedido
import (
"errors"
"fmt"
"time"
"loja/internal/domain/shared"
)
// PedidoID é o identificador do agregado Pedido.
type PedidoID int64
// ItemID é o identificador de um item dentro do pedido.
type ItemID int64
var (
ErrPedidoNaoPodeSerModificado = errors.New("pedido não pode ser modificado neste status")
ErrItemNaoEncontrado = errors.New("item não encontrado no pedido")
ErrQuantidadeInvalida = errors.New("quantidade deve ser maior que zero")
)
// StatusPedido é um Value Object que representa o estado do pedido.
type StatusPedido string
const (
StatusRascunho StatusPedido = "rascunho"
StatusConfirmado StatusPedido = "confirmado"
StatusPago StatusPedido = "pago"
StatusEnviado StatusPedido = "enviado"
StatusEntregue StatusPedido = "entregue"
StatusCancelado StatusPedido = "cancelado"
)
// transicoesPermitidas define quais transições de status são válidas
var transicoesPermitidas = map[StatusPedido][]StatusPedido{
StatusRascunho: {StatusConfirmado, StatusCancelado},
StatusConfirmado: {StatusPago, StatusCancelado},
StatusPago: {StatusEnviado},
StatusEnviado: {StatusEntregue},
}
// Item é uma entidade interna ao agregado Pedido.
// Só pode ser criado/modificado através do Pedido (Aggregate Root).
type Item struct {
id ItemID
produtoID int64
nomeProduto string
quantidade int
precoUnit shared.Dinheiro
}
func (i *Item) ID() ItemID { return i.id }
func (i *Item) ProdutoID() int64 { return i.produtoID }
func (i *Item) NomeProduto() string { return i.nomeProduto }
func (i *Item) Quantidade() int { return i.quantidade }
func (i *Item) PrecoUnit() shared.Dinheiro { return i.precoUnit }
func (i *Item) Subtotal() shared.Dinheiro {
return i.precoUnit.Multiplicar(int64(i.quantidade))
}
// Pedido é o Aggregate Root do agregado de mesmo nome.
// Toda modificação nos itens passa obrigatoriamente pelo Pedido.
type Pedido struct {
id PedidoID
clienteID int64
itens []*Item
status StatusPedido
criadoEm time.Time
eventos []interface{}
proximoItemID ItemID
}
// NovoPedido cria um Pedido em estado de Rascunho.
func NovoPedido(clienteID int64) *Pedido {
return &Pedido{
clienteID: clienteID,
status: StatusRascunho,
criadoEm: time.Now().UTC(),
proximoItemID: 1,
}
}
func ReconstituirPedido(
id PedidoID,
clienteID int64,
itens []*Item,
status StatusPedido,
criadoEm time.Time,
) *Pedido {
proximoID := ItemID(1)
for _, item := range itens {
if item.id >= proximoID {
proximoID = item.id + 1
}
}
return &Pedido{
id: id,
clienteID: clienteID,
itens: itens,
status: status,
criadoEm: criadoEm,
proximoItemID: proximoID,
}
}
func (p *Pedido) ID() PedidoID { return p.id }
func (p *Pedido) ClienteID() int64 { return p.clienteID }
func (p *Pedido) Status() StatusPedido { return p.status }
func (p *Pedido) CriadoEm() time.Time { return p.criadoEm }
func (p *Pedido) Itens() []*Item { return p.itens }
// AdicionarItem adiciona um produto ao pedido.
// Só é permitido quando o pedido está em Rascunho.
func (p *Pedido) AdicionarItem(produtoID int64, nome string, quantidade int, preco shared.Dinheiro) error {
if p.status != StatusRascunho {
return ErrPedidoNaoPodeSerModificado
}
if quantidade <= 0 {
return ErrQuantidadeInvalida
}
// Verifica se o produto já está no pedido — incrementa quantidade
for _, item := range p.itens {
if item.produtoID == produtoID {
item.quantidade += quantidade
return nil
}
}
// Novo item
item := &Item{
id: p.proximoItemID,
produtoID: produtoID,
nomeProduto: nome,
quantidade: quantidade,
precoUnit: preco,
}
p.itens = append(p.itens, item)
p.proximoItemID++
return nil
}
// RemoverItem remove um item do pedido pelo ID do item.
func (p *Pedido) RemoverItem(itemID ItemID) error {
if p.status != StatusRascunho {
return ErrPedidoNaoPodeSerModificado
}
for i, item := range p.itens {
if item.id == itemID {
p.itens = append(p.itens[:i], p.itens[i+1:]...)
return nil
}
}
return ErrItemNaoEncontrado
}
// Total calcula o valor total do pedido.
func (p *Pedido) Total() (shared.Dinheiro, error) {
total := shared.DinheiroEmReais(0)
for _, item := range p.itens {
var err error
total, err = total.Adicionar(item.Subtotal())
if err != nil {
return shared.Dinheiro{}, err
}
}
return total, nil
}
// TransicionarStatus altera o status seguindo as regras de transição.
func (p *Pedido) TransicionarStatus(novoStatus StatusPedido) error {
permitidos, existe := transicoesPermitidas[p.status]
if !existe {
return fmt.Errorf("%w: %s não permite transições", ErrPedidoNaoPodeSerModificado, p.status)
}
for _, permitido := range permitidos {
if permitido == novoStatus {
p.status = novoStatus
p.registrarEvento(StatusAlterado{
PedidoID: p.id,
StatusAnterior: p.status,
StatusNovo: novoStatus,
OcorridoEm: time.Now().UTC(),
})
return nil
}
}
return fmt.Errorf("%w: %s → %s não é permitido", ErrPedidoNaoPodeSerModificado, p.status, novoStatus)
}
// Eventos de domínio
type StatusAlterado struct {
PedidoID PedidoID
StatusAnterior StatusPedido
StatusNovo StatusPedido
OcorridoEm time.Time
}
func (p *Pedido) Eventos() []interface{} {
eventos := p.eventos
p.eventos = nil
return eventos
}
func (p *Pedido) registrarEvento(e interface{}) {
p.eventos = append(p.eventos, e)
}
Domain Services: lógica que não pertence a uma entidade
Quando uma operação de domínio envolve múltiplos agregados e não pertence naturalmente a nenhum deles, ela vai para um Domain Service:
// internal/domain/servico/politica_desconto.go
package servico
import (
"loja/internal/domain/cliente"
"loja/internal/domain/pedido"
"loja/internal/domain/shared"
)
// PoliticaDesconto calcula descontos baseados em regras de negócio
// que envolvem tanto o cliente quanto o pedido.
type PoliticaDesconto struct{}
func (s *PoliticaDesconto) Calcular(
cli *cliente.Cliente,
ped *pedido.Pedido,
) (shared.Dinheiro, error) {
total, err := ped.Total()
if err != nil {
return shared.Dinheiro{}, err
}
// Regra 1: cliente VIP recebe 10% de desconto
// Regra 2: pedidos acima de R$ 500 recebem 5% de desconto
// Regras são cumulativas
desconto := shared.DinheiroEmReais(0)
if cli.EhVIP() {
dez := total.Multiplicar(10)
porCento, _ := shared.NovoDinheiro(dez.Valor()/100, "BRL")
desconto, _ = desconto.Adicionar(porCento)
}
if total.Valor() >= 50000 { // R$ 500,00 em centavos
cinco := total.Multiplicar(5)
porCento, _ := shared.NovoDinheiro(cinco.Valor()/100, "BRL")
desconto, _ = desconto.Adicionar(porCento)
}
return desconto, nil
}
Bounded Contexts: fronteiras explícitas
Em sistemas grandes, o mesmo conceito pode ter significados diferentes em contextos diferentes. Um "Cliente" no contexto de vendas tem atributos diferentes de um "Cliente" no contexto de suporte.
loja/
├── internal/
│ ├── vendas/ ← Bounded Context: Vendas
│ │ ├── domain/
│ │ │ ├── cliente.go ← Cliente com histórico de compras
│ │ │ └── pedido.go
│ │ ├── repository/
│ │ └── usecase/
│ │
│ ├── suporte/ ← Bounded Context: Suporte
│ │ ├── domain/
│ │ │ └── cliente.go ← Cliente com tickets de suporte
│ │ ├── repository/
│ │ └── usecase/
│ │
│ └── catalogo/ ← Bounded Context: Catálogo
│ ├── domain/
│ │ └── produto.go ← Produto com atributos de catálogo
│ ├── repository/
│ └── usecase/
Eventos de domínio: comunicação entre contextos
// internal/shared/eventbus/bus.go
package eventbus
import "sync"
type Handler func(evento interface{})
type Bus struct {
mu sync.RWMutex
handlers map[string][]Handler
}
func Novo() *Bus {
return &Bus{handlers: make(map[string][]Handler)}
}
func (b *Bus) Registrar(nomeEvento string, handler Handler) {
b.mu.Lock()
defer b.mu.Unlock()
b.handlers[nomeEvento] = append(b.handlers[nomeEvento], handler)
}
func (b *Bus) Publicar(nomeEvento string, evento interface{}) {
b.mu.RLock()
handlers := b.handlers[nomeEvento]
b.mu.RUnlock()
// Assíncrono: cada handler roda na sua goroutine, em paralelo com os
// outros, e Publicar retorna antes de eles terminarem. Para síncrono,
// remova o go
for _, h := range handlers {
go h(evento)
}
}
O caso de uso publica os eventos só depois de persistir o cliente, para que ninguém reaja a um cadastro que não chegou ao banco:
// internal/usecase/cliente/criar.go (trecho) — publica eventos após persistir
func (uc *CriarClienteUseCase) Executar(ctx context.Context, input Input) (*Output, error) {
cli, err := cliente.NovoCliente(input.Nome, input.Email, input.Telefone)
if err != nil {
return nil, err
}
if err := uc.repo.Criar(ctx, cli); err != nil {
return nil, err
}
// Publica eventos APÓS a persistência bem-sucedida
for _, evento := range cli.Eventos() {
switch e := evento.(type) {
case cliente.ClienteCriado:
uc.bus.Publicar(e.NomeEvento(), e)
}
}
return &Output{ID: int64(cli.ID())}, nil
}
O go h(evento) tem consequências que precisam ser conhecidas. Num teste com um handler que levava 1 ms, logo depois de publicar 1.000 eventos só 379 tinham sido processados, e até 241 handlers rodaram ao mesmo tempo; os 1.000 só chegaram ao fim porque o processo continuou vivo. Por isso cada handler precisa ser seguro para concorrência: o mesmo teste, com um contador comum no lugar de um atomic, foi acusado pelo -race. Eventos publicados pouco antes de o processo encerrar se perdem, e um panic num handler derruba o programa inteiro, porque acontece numa goroutine sem recover. Para eventos que não podem se perder, o caminho habitual é gravar o evento no banco, na mesma transação da mudança, e publicá-lo depois a partir dali, o padrão conhecido como outbox.
DDD é menos sobre pastas e mais sobre linguagem: o código usa os termos do negócio, as entidades protegem as próprias regras, os Value Objects como Dinheiro e CPF não deixam um valor inválido existir, e o agregado é a única porta para mudar o que precisa mudar junto. Em Go, os campos não exportados e os construtores fazem esse papel sem nenhum framework. O que sustenta o modelo, porém, é o rigor nas regras pequenas: um CPF com dígitos repetidos aceito pelo cálculo, uma regra que cita um conceito que a entidade não tem, um evento entregue numa goroutine que ninguém espera. Testes de domínio, que rodam sem banco e em milissegundos, são o lugar natural para pegar esses casos.
Fontes e leituras recomendadas
-
Domain-Driven Design — Eric Evans — O livro original e fundamental. https://www.domainlanguage.com/ddd/
-
Implementing DDD — Vaughn Vernon — Versão mais acessível e prática. https://vaughnvernon.com/
-
DDD in Go — artigo de referência — Aplicação prática de DDD com Go. https://threedots.tech/post/ddd-lite-in-go-introduction/
-
ThreeDots Tech Blog — Blog com artigos aprofundados sobre DDD e Go. https://threedots.tech
-
go-ddd — exemplo de referência — Repositório com DDD completo em Go. https://github.com/marcusolsson/goddd
-
EventStorming — Técnica de modelagem de domínio colaborativa. https://www.eventstorming.com
Exercícios
Exercício 1
A primeira versão do validarCPF deste artigo aceitava 111.111.111-11 e 000.000.000-00. O cálculo dos dígitos verificadores estava correto. Como esses números passaram, e por que o Value Object é o lugar certo da correção?
Ver resposta
✓ Resposta: Num CPF com os onze dígitos iguais, as somas ponderadas são múltiplos do próprio dígito, e os dois verificadores calculados coincidem com os que estão no número. Matematicamente, eles passam; pela regra da Receita, não são CPFs válidos. Num teste, a versão antiga aceitou 111.111.111-11 e 000.000.000-00, e recusou corretamente 529.982.247-26, com o último dígito trocado. A versão do artigo recusa, antes do cálculo, qualquer número com os onze dígitos iguais, e no mesmo teste os dois foram recusados. O Value Object é o lugar certo porque toda criação de um CPF passa por NovoCPF: corrigida ali, a regra vale para cadastros, importações e qualquer outro caminho, e nenhuma parte do sistema precisa lembrar de repeti-la.
Exercício 2
A PoliticaDesconto chamava cli.EhVIP(), mas o Cliente não tinha esse método nem nenhum dado sobre VIP. O que o erro de compilação revela sobre o modelo, e onde a informação de VIP deveria morar?
Ver resposta
✓ Resposta: O compilador acusou cli.EhVIP undefined (type *cliente.Cliente has no field or method EhVIP), mas o problema não é de digitação: a regra de negócio falava de um conceito, cliente VIP, que o modelo não conhecia. Em DDD, isso é uma lacuna na linguagem compartilhada, e a correção começa por perguntar ao negócio o que torna um cliente VIP e quem decide isso. O artigo resolveu com um estado no próprio Cliente: o campo vip, o método EhVIP e uma operação TornarVIP, que recusa clientes inativos, mais o parâmetro correspondente em ReconstituirCliente. Se ser VIP dependesse do histórico de compras, a resposta seria outra: um serviço de domínio calcularia a condição a partir dos pedidos. Com a regra no lugar, um cliente VIP com um pedido de R$ 600,00 recebeu R$ 90,00 de desconto, os 10% de VIP mais os 5% acima de R$ 500.
Exercício 3
Um handler registrado no event bus incrementa um contador comum, sem sincronização, a cada evento. Num teste, depois de 1.000 eventos publicados, o contador marca menos de 1.000 mesmo depois de uma espera. O que está acontecendo?
Ver resposta
✓ Resposta: O Publicar dispara cada handler com go h(evento), e com 1.000 eventos há centenas de goroutines executando o mesmo handler ao mesmo tempo: num teste com um handler de 1 ms, chegaram a 241 simultâneas. Um contador++ comum é ler, somar e gravar, e duas goroutines que leem o mesmo valor gravam o mesmo resultado, perdendo um incremento. Num teste com o contador comum, o go test -race acusou DATA RACE, e o total ficou abaixo de 1.000; com atomic.Int64, chegou a 1.000. A correção é tornar o handler seguro para concorrência, com atomic, mutex ou canal, ou fazer o bus entregar os eventos de forma sequencial. O Registrar e o Publicar do bus usam um RWMutex, mas ele protege só o mapa de handlers, e não o que cada handler faz.
Exercício 4
Um comando de linha de comando cria um cliente e termina logo em seguida. O evento ClienteCriado deveria disparar um e-mail de boas-vindas, mas o e-mail às vezes não sai. O que o event bus tem a ver com isso, e como garantir a entrega?
Ver resposta
✓ Resposta: O Publicar retorna assim que dispara as goroutines, sem esperar por elas. Num teste com 1.000 eventos, só 379 tinham sido processados no momento em que o Publicar do último retornou. Quando o main termina, o programa encerra junto com todas as goroutines em andamento, e o handler do e-mail pode nem ter começado. Num servidor que roda continuamente isso quase não aparece, e por isso o defeito passa despercebido até alguém usar o mesmo caso de uso num processo curto. Há duas saídas, de alcance diferente. Uma é o bus acompanhar as goroutines com um sync.WaitGroup e oferecer um método para esperá-las antes de encerrar. A outra, que também resiste a quedas e reinícios, é o padrão outbox: gravar o evento numa tabela, na mesma transação do cliente, e deixar um processo separado ler e entregar os eventos pendentes.
Exercício 5
Um desenvolvedor sugere tirar o ReconstituirCliente e carregar clientes do banco com NovoCliente, "para não duplicar código". Que efeitos isso teria?
Ver resposta
✓ Resposta: Os dois construtores representam momentos diferentes da vida da entidade. NovoCliente representa o nascimento: valida o nome e o e-mail, define criadoEm como agora, marca o cliente como ativo e registra o evento ClienteCriado. Usá-lo para carregar um cliente do banco teria três efeitos. Todo carregamento registraria um ClienteCriado novo, e, se os eventos fossem publicados, cada consulta dispararia de novo o e-mail de boas-vindas. A data de criação e os estados ativo e vip se perderiam, substituídos pelos valores iniciais. E um registro antigo que não passasse nas validações de hoje, como um e-mail aceito por uma regra mais frouxa no passado, deixaria de poder ser carregado. O ReconstituirCliente recebe o estado completo e não dispara eventos, porque a entidade já existia; a duplicação aparente é a diferença entre criar e recuperar.