Trilha bônus · IA para arquitetura · ~32 min · pleno/sênior

Depois de mapear as fronteiras do ERP legado (o B1), veio a parte mais delicada: dentro de cada bounded context, o modelo era um caos. Uma "transação" de venda podia ser alterada por sete lugares diferentes, sem nenhuma regra garantindo que ela ficasse consistente — um cancelava, outro reembolsava, um terceiro mexia no total, e ninguém protegia as invariantes ("o total tem que bater com a soma dos itens"). Além disso, uma única transação de banco abrangia a venda inteira — criar pedido, baixar estoque, faturar, notificar — segurando locks por segundos e travando o sistema sob carga (o antipadrão da aula Transações e consistência).

O DDD tático (aggregates, domain events, e a redução de escopo transacional que eles permitem) é o remédio para isso. Mas aplicá-lo em 400 mil linhas à mão é inviável. Usei IA de novo — e a lição do B1 se confirmou com uma nuance importante: no tático, a IA é ainda mais útil que no estratégico, porque muito do trabalho é mecânico e verificável (identificar quem muda o quê, onde estão as invariantes, quais escritas precisam ser atômicas). Mas o padrão continua: a IA propõe o aggregate e os eventos; você valida se as invariantes que ela achou são as invariantes reais do negócio — e mede o resultado, porque no tático o benefício é quantificável (escopo de transação reduzido, contenção medida). Este bônus fecha o curso mostrando como a IA acelera o DDD tático e como transformar refatoração em ROI que você mostra num gráfico.

DDD tático em uma página: aggregate como fronteira de consistência

O DDD estratégico (B1, aula DDD estratégico + Arquitetura Hexagonal no FakeFlix) decide as fronteiras entre contextos. O DDD tático decide como modelar dentro de um contexto. A peça central é o aggregate, e sua definição operacional é a que importa:

Um aggregate é um conjunto de objetos tratados como uma unidade de consistência: ele tem uma raiz (a aggregate root), toda mudança passa por ela, e ela garante as invariantes (as verdades que devem valer sempre) do conjunto. De fora, você só fala com a raiz — nunca com as partes internas diretamente.

Dois problemas que o aggregate resolve, ambos vistos no curso:

Essa segunda ideia é poderosíssima e é a ponte para o ROI. No ERP, a "transação de venda" era um aggregate gigante que abrangia pedido + estoque + fatura. Ao reconhecer que são três aggregates (Pedido, Estoque, Fatura), cada um sua fronteira de consistência, a transação de banco que segurava tudo por segundos se quebra em três transações curtas, coordenadas por domain events com consistência eventual. Os locks caem de segundos para milissegundos. Isso é DDD tático desenhando o escopo transacional — exatamente o gancho da aula Transações e consistência ("os limites da transação desenham os limites dos módulos") descendo ao nível do aggregate.

Domain events: comunicando dentro e entre aggregates

O domain event é o par do aggregate. Quando a raiz de um aggregate faz algo relevante ao negócio, ela registra um evento de domínio ("PedidoConfirmado", "PagamentoRecebido"). Outros aggregates (no mesmo contexto ou em contextos vizinhos) reagem a esse evento — em vez de serem modificados na mesma transação.

// ✅ aggregate root que protege invariantes E registra domain events
export class Order {                    // aggregate root
  private items: OrderItem[] = [];      // parte interna — não se mexe de fora
  private status: OrderStatus = 'draft';
  private _events: DomainEvent[] = [];

  addItem(product: ProductId, qty: number, price: Money) {
    if (this.status !== 'draft') throw new OrderNotEditableError(); // INVARIANTE
    this.items.push(new OrderItem(product, qty, price));
  }

  confirm() {
    if (this.items.length === 0) throw new EmptyOrderError();       // INVARIANTE
    this.status = 'confirmed';
    // registra o FATO — não modifica estoque/fatura aqui (outra transação)
    this._events.push(new OrderConfirmedEvent(this.id, this.total()));
  }

  private total(): Money {
    return this.items.reduce((sum, i) => sum.add(i.subtotal()), Money.zero());
  }
  pullEvents(): DomainEvent[] { const e = this._events; this._events = []; return e; }
}

Repare: Order protege suas invariantes (não edita se confirmado, não confirma vazio, total sempre derivado dos itens) e, ao confirmar, registra OrderConfirmedEvent em vez de ir mexer em estoque e fatura. Quem baixa o estoque e emite a fatura são outros aggregates, reagindo ao evento numa transação separada — via o Transactional Outbox da aula Event-Driven na prática: Transactional Outbox, idempotência e sagas, para que o evento nunca se perca. É a saga (aula Event-Driven na prática: Transactional Outbox, idempotência e sagas) nascendo naturalmente do modelo tático: cada aggregate cuida da sua consistência forte; entre aggregates, consistência eventual por eventos.

IA para identificar aggregates e invariantes

Aqui a IA brilha, porque a maior parte do trabalho é detetivesco e verificável: rastrear no código quem muda o quê e onde as regras de consistência estão (ou deveriam estar). A IA lê o legado e propõe: candidatos a aggregate, suas raízes, e — o mais valioso — as invariantes implícitas que estão espalhadas pelo código sem dono.

Você é um analista de DDD tático. Abaixo está o código de um bounded context legado.

Tarefa:
1. Identifique candidatos a AGGREGATE: conjuntos de entidades que sempre mudam juntas e
   precisam manter consistência entre si. Indique a aggregate root de cada um.
2. Extraia INVARIANTES: regras que o código verifica (ou DEVERIA verificar) para manter a
   consistência — ex.: "total do pedido = soma dos itens", "não confirmar pedido vazio".
   Cite onde cada verificação aparece HOJE (ou marque "não protegida" se a regra é violável).
3. Identifique operações que HOJE modificam múltiplos candidatos a aggregate na MESMA
   transação — são alvos para quebrar em transação-por-aggregate + domain event.
4. Proponha os DOMAIN EVENTS que ligariam esses aggregates com consistência eventual.
5. Marque [INCERTO] onde a invariante for ambígua e explique. Não invente invariante de
   negócio que o código não sugere.

[código do contexto...]

O item 2 é ouro: a IA encontra invariantes que estão espalhadas e desprotegidas — a regra "total = soma dos itens" verificada em três lugares e esquecida num quarto (onde nasce o bug). Ela mapeia onde a regra é honrada e onde é violável, o que te dá exatamente a lista do que o aggregate precisa centralizar. O item 3 acha as transações largas (aula Transações e consistência) automaticamente: operações que tocam múltiplos aggregates candidatos numa transação só. E o item 4 propõe os eventos que substituem essa atomicidade por consistência eventual.

O que você valida (o julgamento humano, de novo): a IA vai propor invariantes a partir do que o código faz. Mas o código pode estar errado — pode faltar uma invariante que o negócio exige (e por isso há bugs), ou ter uma "invariante" que era só um acidente de implementação. Só quem conhece o negócio distingue "esta regra é uma verdade do domínio" de "isto era um workaround". A IA acha os candidatos a invariante lendo o código; você confirma quais são invariantes reais confrontando com o negócio — o mesmo princípio do B1, agora no nível tático.

Reduzindo escopo de transação com Outbox (a IA acha os pontos)

Identificados os aggregates, o ganho concreto vem de quebrar as transações largas. O padrão (aula Transações e consistência + 15): cada operação modifica um aggregate numa transação curta e grava um domain event no outbox (mesma transação); os outros aggregates reagem ao evento em suas próprias transações curtas.

Antes (uma transação abraça três aggregates — locks por segundos):

// ❌ o antipadrão do ERP: tudo atômico, locks longos, trava sob carga
@Transactional()
async completeSale(dto) {
  const order = await this.orders.create(dto);        // aggregate 1
  await this.inventory.decrement(dto.items);          // aggregate 2 — na MESMA transação
  await this.invoicing.issue(order);                  // aggregate 3 — idem
  await this.notifier.sendConfirmation(order);        // 🐌 I/O de rede DENTRO da transação
}

Depois (uma transação por aggregate + eventos via outbox):

// ✅ transação curta: só o aggregate Order + o evento no outbox (aula *Event-Driven na prática: Transactional Outbox, idempotência e sagas*)
@Transactional()
async confirmOrder(dto) {
  const order = Order.create(dto);
  order.confirm();                                    // protege invariantes, registra evento
  await this.orders.save(order);                      // aggregate 1, transação curta
  await this.outbox.saveAll(order.pullEvents());      // OrderConfirmedEvent, mesma transação
} // COMMIT em milissegundos

// estoque reage — SUA transação, SEU aggregate
@OnEvent('OrderConfirmed')
async onOrderConfirmed(e: OrderConfirmedEvent) {      // idempotente (aula *Event-Driven na prática: Transactional Outbox, idempotência e sagas*)
  await this.inventory.decrementFor(e.orderId);       // aggregate 2, transação curta separada
}
// faturamento e notificação reagem igual, cada um na sua transação/consumidor

A IA ajuda a achar esses pontos em escala: dado o mapa de aggregates, ela sinaliza cada método @Transactional (ou BEGIN...COMMIT) que toca mais de um aggregate e propõe o corte (qual escrita fica na transação, qual vira evento). Num legado com centenas de transações, isso é semanas de trabalho comprimidas — e verificável, porque você audita a proposta contra o mapa de aggregates que já validou.

ROI mensurável: transformando refatoração em gráfico

A diferença entre o DDD tático e muita refatoração "de fé" é que aqui o benefício é quantificável — e comunicar isso em números é o que consegue tempo e apoio para continuar. Métricas concretas de antes/depois:

  • Escopo de transação: duração média de uma transação (do BEGIN ao COMMIT) e número de aggregates/tabelas que ela toca. A quebra por aggregate derruba os dois. No ERP, transações de venda caíram de ~2s para ~30ms de lock — o "reduzir transaction scope em 90%" deixa de ser slogan e vira medição.
  • Contenção/tempo de lock: tempo médio que transações esperam por locks, e taxa de deadlocks/timeouts. Transações curtas seguram locks por menos tempo → menos contenção. Mede-se direto nas métricas do banco (pg_stat_activity, tempo de espera de lock).
  • Throughput sob carga: transações/segundo que o sistema sustenta antes de degradar. Locks curtos = mais concorrência = mais throughput.
  • Bugs de invariante: incidentes onde a consistência foi violada (total que não bate, pedido confirmado vazio). Aggregates que protegem invariantes → esses bugs somem, e você conta a queda.
ROI da refatoração de aggregates (exemplo real, antes → depois):
  duração de lock (p95):        1.900ms  →  35ms     (-98%)
  aggregates por transação:     3.0      →  1.0
  deadlocks/dia:                40       →  1
  throughput sob pico:          220 tx/s →  1.400 tx/s
  bugs de invariante/mês:       12       →  0

Esse quadro é o que você leva para a liderança. Refatoração de arquitetura costuma ser difícil de justificar ("vai melhorar a manutenibilidade" é abstrato); o DDD tático permite dizer "reduzimos o tempo de lock em 98% e eliminamos os deadlocks, sustentando 6x mais carga" — evidência (aula Por que não existe arquitetura perfeita) que fala a língua do negócio. A IA ajuda aqui também: extraindo as métricas de antes (dos logs/métricas do banco), estimando o impacto por ponto de refatoração e priorizando pela relação impacto/esforço — a mesma matriz de risco do B1, agora orientada a ROI.

O fecho do curso: o DDD tático assistido por IA une as duas metades do que ensinamos. A IA comprime o trabalho braçal (achar aggregates, invariantes, transações largas) para você gastar seu tempo no julgamento (as invariantes são reais? a fronteira do aggregate faz sentido no negócio?) — e o resultado é medível, então a arquitetura evolutiva deixa de ser um argumento de fé e vira um retorno demonstrável. Comece simples, evolua com evidência — e agora você tem a evidência em números.

Antes → Depois

Antes Depois
Modelo anêmico: sete lugares mutam o mesmo objeto Aggregate root centraliza mutação e protege invariantes
Uma transação abraça a venda inteira (locks por segundos) Uma transação por aggregate (locks em ms) + domain events
Invariantes espalhadas e violáveis Invariantes protegidas na raiz do aggregate
Achar aggregates/transações largas à mão (semanas) IA propõe candidatos e pontos de corte; você valida
"Vai melhorar a manutenibilidade" (abstrato) "-98% de lock, -97% de deadlock, 6x throughput" (ROI)

Tradeoffs e armadilhas

  • Aggregate grande demais recria a transação larga. Se você desenha uma fronteira de aggregate que engloba coisas demais, volta ao problema de locks longos e contenção. O aggregate deve ser o menor conjunto que precisa ser consistente de forma forte; o resto conversa por evento. A IA pode propor aggregates inchados (agrupando por proximidade de código); você os enxuga pela regra "uma transação, um aggregate".
  • Consistência eventual entre aggregates tem custo (aula Event-Driven na prática: Transactional Outbox, idempotência e sagas). Quebrar a transação larga em eventos introduz Outbox, idempotência, sagas e a janela de consistência eventual. Isso só compensa onde a contenção era dor real — não modularize aggregates "por pureza" num sistema que não tem problema de lock. O ROI tem que ser medido, não presumido.
  • A IA sugere invariantes do código, não do negócio. O código legado pode estar errado — faltando invariantes (por isso os bugs) ou tendo pseudo-invariantes acidentais. Aceitar as invariantes da IA sem validar com o negócio congela os bugs atuais como se fossem regras. Sempre confirme "isto é uma verdade do domínio?" com quem sabe.
  • Métricas de ROI podem ser gameadas (Goodhart). Otimizar "reduzir duração de transação" a qualquer custo pode levar a partir uma operação que deveria ser atômica (criando o buraco de consistência da aula Transações e consistência). A meta é o design correto; as métricas são evidência de que ele está funcionando, não o alvo em si.
  • DDD tático não conserta fronteira estratégica errada. Se o bounded context está errado (B1), nenhum aggregate bem desenhado salva. O tático opera dentro de uma fronteira estratégica correta; erre a estratégica e você estará organizando lindamente o modelo errado.

Cola mental

  • Aggregate = unidade de consistência com uma raiz que protege as invariantes; de fora, só se fala com a raiz. Regra de ouro: uma transação modifica um aggregate.
  • Aggregate resolve modelo anêmico (aula Clean Architecture: o que aproveitar e o que ignorar: a lógica volta para junto do dado) e transação larga (aula Transações e consistência: o aggregate é a fronteira natural da transação).
  • Domain events ligam aggregates com consistência eventual (via Outbox — aula Event-Driven na prática: Transactional Outbox, idempotência e sagas), substituindo a atomicidade entre eles. Cada aggregate: consistência forte interna; entre eles: eventual.
  • IA no tático: acha aggregates candidatos, invariantes espalhadas/desprotegidas e transações que tocam múltiplos aggregates (verificável). Você valida se as invariantes são reais no negócio (o código pode estar errado).
  • Reduzir escopo transacional: uma transação por aggregate + evento no outbox → locks de segundos para ms.
  • ROI mensurável: duração de lock, contenção/deadlocks, throughput, bugs de invariante — antes/depois. É a evidência (aula Por que não existe arquitetura perfeita) que fala a língua do negócio.
  • Cuidados: aggregate inchado recria a transação larga; consistência eventual custa (só onde há dor); IA sugere invariante do código, não do negócio; tático não conserta estratégico errado (B1).

Referências para Aprofundamento

DDD tático

Transformar refatoração em número

  • Accelerate — Nicole Forsgren, Jez Humble e Gene Kim — As quatro métricas (frequência de deploy, lead time, taxa de falha, tempo de recuperação) que dão linguagem comum para mostrar retorno a quem decide orçamento.
  • DORA — Guia das métricas — A referência atualizada e gratuita das mesmas métricas.
  • CodeScene — Útil aqui pelo lado do "antes e depois": mostrar a queda de coacoplamento após a refatoração é o gráfico que a aula pede.

Exercícios

Exercício 1 — Desenhe o aggregate

No FakeFlix, uma Subscription tem: status, ciclo (início/fim), método de pagamento e um histórico de faturas. As faturas, uma vez emitidas, são imutáveis e consultadas por relatórios independentes. Quem é a aggregate root? As faturas fazem parte do aggregate Subscription ou são um aggregate separado? Justifique com a regra "uma transação, um aggregate" e com a aula Transações e consistência.

Ver resposta

✓ Resposta: Aggregate root: Subscription. As faturas são um aggregate separado (cada Invoice é sua própria raiz), não parte do aggregate Subscription. Justificativa pela regra "uma transação, um aggregate" (aula Transações e consistência): as faturas são imutáveis após emitidas e consultadas por relatórios independentes — elas não precisam mudar consistentemente junto com mudanças de status/ciclo da assinatura. Se Invoice estivesse dentro do aggregate Subscription, cada operação na assinatura carregaria e bloquearia todo o histórico de faturas (aggregate inchado → transação larga → contenção). Mantendo-as separadas: mudar o status da assinatura é uma transação curta sobre Subscription; emitir uma fatura é uma transação curta sobre Invoice, disparada por um domain event (ex.: SubscriptionRenewed → emite Invoice). Consistência forte dentro de cada aggregate, eventual entre eles. A ligação é por referência de id (Invoice guarda o subscriptionId), não por composição.

Exercício 2 — Ache a invariante desprotegida

A IA reporta que, no código legado, a regra "o total do pedido é a soma dos subtotais dos itens" é verificada em OrderController.create e em OrderService.addItem, mas não em OrderService.applyDiscount, que altera o total diretamente. O que isso indica? Como o aggregate root corrige, e por que a IA foi útil aqui?

Ver resposta

✓ Resposta: Indica uma invariante desprotegida: "total = soma dos subtotais" é honrada em dois caminhos e violada em applyDiscount, que muta o total diretamente sem recalcular — é aí que nasce o bug (total que não bate). O problema-raiz é o modelo anêmico (aula Clean Architecture: o que aproveitar e o que ignorar): o total é um dado exposto que múltiplos serviços mutam, sem um dono garantindo a regra. Correção pelo aggregate root: o total deixa de ser setável de fora; vira uma propriedade derivada pela raiz Order (total() = soma dos itens, sempre), e applyDiscount passa a ser um método da raiz que ajusta via um item/campo de desconto e recalcula o total mantendo a invariante — nenhum caminho externo consegue mais setar o total à revelia. Por que a IA foi útil: ela varreu todos os caminhos que tocam o total e apontou exatamente o que estava desprotegido (applyDiscount), um levantamento que à mão exigiria rastrear cada mutação do campo em todo o legado — trabalho mecânico e verificável, a força da IA no tático.

Exercício 3 — Quebre a transação larga

Reescreva conceitualmente esta operação em transação-por-aggregate + domain events, dizendo quais escritas ficam atômicas juntas e quais viram eventos:

@Transactional()
async placeOrder(dto) {
  const order = await this.orders.create(dto);   // aggregate Order
  await this.wallet.debit(dto.userId, order.total); // aggregate Wallet
  await this.inventory.reserve(dto.items);        // aggregate Inventory
  await this.email.sendReceipt(order);            // I/O externo
}

Qual dessas quebras exige idempotência no consumidor, e por quê (aula Event-Driven na prática: Transactional Outbox, idempotência e sagas)?

Ver resposta

✓ Resposta: Quebra em transação-por-aggregate + eventos:

// transação curta 1: só o aggregate Order + evento no outbox
@Transactional()
async placeOrder(dto) {
  const order = Order.create(dto);
  order.place();                                  // invariantes; registra OrderPlacedEvent
  await this.orders.save(order);
  await this.outbox.saveAll(order.pullEvents());  // mesma transação
}
// Wallet reage — sua transação, seu aggregate
@OnEvent('OrderPlaced') async debit(e) { await this.wallet.debitFor(e.orderId, e.total); }
// Inventory reage — sua transação
@OnEvent('OrderPlaced') async reserve(e) { await this.inventory.reserveFor(e.orderId); }
// e-mail reage — fora de qualquer transação de banco
@OnEvent('OrderPlaced') async receipt(e) { await this.email.sendReceipt(e.orderId); }

Escritas atômicas juntas: só Order + seus eventos no outbox. Débito de carteira, reserva de estoque e e-mail viram reações a OrderPlacedEvent, cada uma na sua transação/consumidor. Idempotência é exigida em todos os consumidores (o evento pode ser entregue mais de uma vez sob at-least-once — aula Event-Driven na prática: Transactional Outbox, idempotência e sagas), mas criticamente no débito da carteira: processar OrderPlaced duas vezes não pode debitar duas vezes (efeito aditivo — a cobrança dupla das aulas Transações e consistência e Event-Driven na prática: Transactional Outbox, idempotência e sagas). Use dedup por messageId ou operação idempotente (registrar o débito com o orderId como chave única). O e-mail também deve ser idempotente (não enviar dois recibos), embora o dano seja menor. (Observação: débito de carteira e reserva de estoque agora são eventualmente consistentes com o pedido — se um falhar, entra compensação/saga, aula Event-Driven na prática: Transactional Outbox, idempotência e sagas; confirme que o negócio tolera essa janela, senão parte disso precisa ser forte.)

Exercício 4 — Valide a invariante da IA

A IA, lendo o código, propõe a invariante "um usuário só pode ter uma assinatura ativa por vez", porque o código sempre busca findActiveByUser e assume um resultado. Como você decide se essa é uma invariante real do negócio ou um acidente do código? Que pergunta você faz ao negócio, e o que muda no aggregate em cada resposta?

Ver resposta

✓ Resposta: Você decide confrontando a suposição do código com o negócio, porque o código só mostra o que é feito hoje, não o que a regra deveria ser. A pergunta ao negócio: "um cliente pode, legitimamente, ter duas assinaturas ativas ao mesmo tempo — por exemplo, um plano pessoal e um plano família/presente, ou dois perfis?". Se a resposta é "não, nunca — é uma verdade do negócio": então é uma invariante real, e o aggregate deve protegê-la ativamente (ao ativar uma assinatura, a raiz garante que não existe outra ativa; o banco reforça com uma constraint de unicidade parcial). Se a resposta é "sim, pode acontecer": então o findActiveByUser que assume um resultado é um acidente/bug do código (que quebraria no dia em que houver duas), e a "invariante" da IA é falsa — o aggregate não deve impô-la, e você provavelmente tem um bug latente a corrigir (a busca deveria lidar com múltiplas). Em ambos os casos, a IA foi útil por achar a suposição embutida no código; só o negócio decide se ela é lei ou defeito — o julgamento humano no lugar certo.

Exercício 5 — Comunique o ROI

Você refatorou o aggregate de faturamento, quebrando a transação larga. Antes: duração de lock p95 = 1.500ms, deadlocks/dia = 25, throughput de pico = 300 tx/s. Depois: 40ms, 1, 1.100 tx/s. Escreva o resumo de duas frases que você levaria à liderança para justificar continuar a refatoração nos outros aggregates, conectando os números a impacto de negócio.

Ver resposta

✓ Resposta: Exemplo: "Ao redesenhar o aggregate de faturamento, reduzimos o tempo de lock em 97% (1.500ms → 40ms) e os deadlocks em 96% (25/dia → 1), o que quase quadruplicou a carga que o sistema sustenta no pico (300 → 1.100 transações/segundo) — ou seja, absorvemos o crescimento sem comprar mais infraestrutura. Aplicar a mesma refatoração aos outros aggregates deve estender esse ganho de capacidade e estabilidade ao restante do sistema, com o mesmo padrão já validado." Conecta os números a impacto de negócio (capacidade sem custo de infra, estabilidade) e usa a evidência (aula Por que não existe arquitetura perfeita) para justificar continuar — falando a língua da liderança, não a do banco de dados.