Arquitetura: Clean Architecture, DDD com moderação e os padrões que já usamos

Arquitetura: Clean Architecture, DDD com moderação e os padrões que já usamos

A Livraria olhada como um todo: a regra de dependência de Clean Architecture e arquitetura hexagonal, testes que verificam a arquitetura, DDD com moderação (linguagem ubíqua, objetos de valor, agregados e contextos delimitados), CQRS sem cerimônia, fatias verticais, os padrões com nome e ADRs.
Linguagem C#

• • 25 min de leitura

A Livraria tem hoje uma dúzia de projetos: domínio, aplicação, três bancos, API, site, worker, serviço de frete, caixa, aplicativo e um jogo. Eles surgiram um de cada vez, cada um por um motivo concreto. Este artigo abre a Fase 10 olhando para o conjunto. Damos nomes às ideias que guiaram essas escolhas, apontamos onde a Livraria as segue e onde as dobra, e discutimos quanta arquitetura um sistema realmente precisa.

O mapa atual

                        ┌──────────────────────────┐
                        │     Livraria.Dominio     │  Produto, LivroImpresso, Pedido, regras, eventos
                        └────────────▲─────────────┘
                                     │
                        ┌────────────┴─────────────┐
                        │    Livraria.Aplicacao    │  ServicoDeVendas, portas (interfaces)
                        └──▲──────▲──────▲──────▲──┘
                           │      │      │      │
      ┌────────────────────┘      │      │      └────────────────────┐
┌─────┴──────────┐  ┌─────────────┴──┐ ┌─┴──────────────┐  ┌─────────┴────────┐
│ Dados.EF       │  │ Dados.Mongo    │ │ Livraria.Api   │  │ Livraria.Worker  │
│ .Postgres      │  │                │ │ (HTTP, SignalR)│  │ (RabbitMQ)       │
│ .MySql         │  │                │ │                │  │                  │
└────────────────┘  └────────────────┘ └───────▲────────┘  └──────────────────┘
                                               │ HTTP / gRPC / SignalR
             ┌──────────────┬──────────────┬───┴──────────┬───────────────┐
        Livraria.Site  Livraria.Caixa  Livraria.App  Livraria.Frete  Biblioteca em Apuros

As setas mostram quem depende de quem, e todas apontam na mesma direção: para o centro. O domínio não conhece a aplicação, a aplicação não conhece a API nem os bancos, e os bancos e a API conhecem a aplicação para implementar e usar suas interfaces. Essa direção única não aconteceu por acaso. Ela foi escolhida em "Um repositório, três bancos", quando IRepositorioProdutos foi colocada no domínio e as implementações nos projetos de dados. E ela tem um nome.

A regra de dependência

Três formulações quase idênticas da mesma ideia apareceram em momentos diferentes:

  • Ports and Adapters, ou arquitetura hexagonal, de Alistair Cockburn (2005). A aplicação tem portas, interfaces que ela define, e o mundo externo se conecta por adaptadores. Banco, interface web, fila de mensagens e testes são todos adaptadores, e a aplicação não distingue um do outro.
  • Onion Architecture, de Jeffrey Palermo (2008). O sistema é desenhado em camadas concêntricas, com o modelo de domínio no centro.
  • Clean Architecture, de Robert C. Martin (2012). Os mesmos círculos, com uma regra explícita: dependências de código-fonte só podem apontar para dentro. Nada num círculo interno pode mencionar algo de um círculo externo: nem uma classe, nem uma função, nem um nome.

O que as três protegem é o mesmo: as regras de negócio não podem depender de detalhes. Banco, framework web, fila, motor de jogo, provedor de IA: tudo isso é detalhe, no sentido de que é substituível sem mudar o que a Livraria faz. A Livraria provou isso trocando PostgreSQL por MySQL e por MongoDB sem tocar no ServicoDeVendas, e expondo as mesmas regras por console, HTTP, gRPC, fila, desktop e celular.

A regra tem um custo, e ele apareceu em cada artigo que a aplicou: interfaces a mais, mapeamentos entre entidades e DTOs, adaptadores que só traduzem. Em troca, as regras podem ser testadas sem infraestrutura, a infraestrutura pode mudar sem mexer nas regras, e cada peça pode ser entendida sozinha.

Onde a Livraria dobra a regra

Uma avaliação honesta encontra alguns pontos em que a regra foi flexibilizada, e vale a pena saber quais são e por quê:

  • InternalsVisibleTo("Livraria.Dados.Mongo") no domínio, de "Um repositório, três bancos". O domínio nomeia um projeto de infraestrutura para liberar a Produto.AtribuirIdentidade. É uma dependência, ainda que só no atributo, e o compilador não a vê como tal. A alternativa limpa seria um construtor de reidratação público mas explicitamente documentado, ou uma fábrica no domínio. Foi uma troca consciente de pureza por encapsulamento.
  • [Required] e [StringLength] nos DTOs de entrada. Eles vivem nos contratos, e não no domínio, então não violam a regra. Mas as mesmas regras existem nas invariantes do domínio, duplicadas de propósito, como discutimos em "Validação, ProblemDetails e OpenAPI".
  • System.Diagnostics.Activity e Meter dentro do ServicoDeVendas, de "Produção: CORS, rate limiting, versionamento e OpenTelemetry". Não é uma dependência do OpenTelemetry, e sim da BCL, que o domínio pode usar. A decisão foi aceitar a BCL como parte da linguagem, e não como infraestrutura.

Nenhum desses pontos é um defeito grave. Arquitetura é um conjunto de trocas, e o importante é que elas sejam deliberadas e visíveis, e não acidentes descobertos anos depois.

Testando a arquitetura

Regras de arquitetura que só existem num diagrama se perdem na primeira pressa. Elas podem ser verificadas como qualquer outro requisito, com testes:

using NetArchTest.Rules;

public sealed class ArquiteturaTestes
{
    private static readonly string[] Infraestrutura =
    [
        "Microsoft.EntityFrameworkCore", "Npgsql", "MySqlConnector", "MongoDB",
        "Microsoft.AspNetCore", "StackExchange.Redis", "RabbitMQ.Client", "Godot"
    ];

    [Fact]
    public void Dominio_nao_depende_de_infraestrutura()
    {
        var resultado = Types.InAssembly(typeof(Pedido).Assembly)
            .ShouldNot().HaveDependencyOnAny(Infraestrutura)
            .GetResult();

        Assert.True(resultado.IsSuccessful,
            "Tipos com dependência proibida: " + string.Join(", ", resultado.FailingTypeNames ?? []));
    }

    [Fact]
    public void Aplicacao_nao_depende_de_infraestrutura_nem_da_api()
    {
        var resultado = Types.InAssembly(typeof(ServicoDeVendas).Assembly)
            .ShouldNot().HaveDependencyOnAny([.. Infraestrutura, "Livraria.Api", "Livraria.Dados"])
            .GetResult();

        Assert.True(resultado.IsSuccessful);
    }

    [Fact]
    public void Endpoints_nao_usam_o_DbContext_diretamente()
    {
        var resultado = Types.InAssembly(typeof(EndpointsDeProdutos).Assembly)
            .That().ResideInNamespace("Livraria.Api.Endpoints")
            .ShouldNot().HaveDependencyOn("Livraria.Dados.EF")
            .GetResult();

        Assert.True(resultado.IsSuccessful);
    }
}

A biblioteca NetArchTest inspeciona os assemblies compilados e verifica as dependências entre tipos. ArchUnitNET, uma adaptação do ArchUnit do Java, oferece a mesma coisa com mais recursos. Esses testes rodam no CI junto com os outros. Um desenvolvedor que injeta o LivrariaContext direto num endpoint "só dessa vez" recebe uma falha de build explicando por quê, em vez de um comentário numa revisão de código que pode passar despercebido. O teste do exercício 4 de "Arquitetura de jogo e por que não alocar dentro do loop", que impedia as regras do jogo de usar o Godot, era a mesma ideia garantida pelo compilador, pela separação em projetos.

DDD, com moderação

Domain-Driven Design é o título do livro de Eric Evans, de 2003, que propôs pôr o modelo do negócio no centro do software, e desenvolvê-lo em conversa contínua com quem entende do negócio. O livro tem duas metades: a estratégica, sobre como dividir um sistema grande e como os pedaços se relacionam, e a tática, sobre os blocos de construção do código. Muitas equipes adotaram só a tática, com todos os padrões em todo lugar, e produziram sistemas mais complicados do que os problemas que resolviam. "DDD leve" é tirar do livro o que se paga no tamanho do sistema, e deixar o resto.

Linguagem ubíqua

A ideia mais barata e mais valiosa do DDD é a linguagem ubíqua: o código usa as mesmas palavras que o negócio usa, com os mesmos significados. A Livraria fez isso desde o primeiro artigo sem nomear: Pedido, ItemPedido, Estoque, VendaDeBalcao, Recorde, e não Order, OrderLine e Inventory numa empresa que fala português. Quando o gerente da loja diz "o balcão vendeu fiado", o código tem ou não tem um conceito para isso, e a conversa é sobre o mesmo objeto.

A linguagem ubíqua também revela ambiguidades. Em "O caixa da Livraria em Avalonia", "estoque" apareceu com dois sentidos: o número no sistema e os livros na prateleira. A regra "o estoque nunca fica negativo" era verdadeira para o primeiro e absurda para o segundo. Resolver a ambiguidade no código obrigou a resolvê-la na conversa com o negócio.

Entidades e objetos de valor

Uma entidade tem identidade que persiste enquanto seus atributos mudam: o pedido 42 continua sendo o pedido 42 depois de cancelado. Um objeto de valor é definido só pelos seus atributos, é imutável, e dois com os mesmos atributos são o mesmo: R$ 39,90 é igual a R$ 39,90, não importa de onde veio.

A Livraria ainda usa string para ISBN e decimal para preço. Isso tem custos que já apareceram: a validação do ISBN existe em três lugares (o atributo [Isbn], o construtor de LivroImpresso, o caixa), e a v2 da API precisou de um objeto Dinheiro com moeda. Objetos de valor concentram essas regras:

public sealed record Isbn
{
    public string Valor { get; }

    private Isbn(string valor) => Valor = valor;

    public static Isbn Criar(string? texto) =>
        TryCriar(texto, out var isbn)
            ? isbn
            : throw new RegraDeNegocioException($"ISBN inválido: '{texto}'.");

    public static bool TryCriar(string? texto, [NotNullWhen(true)] out Isbn? isbn)
    {
        isbn = null;
        if (texto is null) return false;

        Span<char> digitos = stackalloc char[13];
        var n = 0;
        foreach (var c in texto)
        {
            if (!char.IsAsciiDigit(c)) continue;
            if (n == 13) return false;
            digitos[n++] = c;
        }
        if (n != 13 || !DigitoVerificadorConfere(digitos)) return false;

        isbn = new Isbn(new string(digitos));
        return true;
    }

    private static bool DigitoVerificadorConfere(ReadOnlySpan<char> d)
    {
        var soma = 0;
        for (var i = 0; i < 12; i++) soma += (d[i] - '0') * (i % 2 == 0 ? 1 : 3);
        return (10 - soma % 10) % 10 == d[12] - '0';
    }

    public override string ToString() => Valor;
}
public readonly record struct Dinheiro(decimal Valor, Moeda Moeda)
{
    public static Dinheiro Reais(decimal valor) => new(decimal.Round(valor, 2), Moeda.BRL);

    public static Dinheiro operator +(Dinheiro a, Dinheiro b) =>
        a.Moeda == b.Moeda ? new(a.Valor + b.Valor, a.Moeda)
                           : throw new InvalidOperationException($"Não se somam {a.Moeda} e {b.Moeda}.");

    public static Dinheiro operator *(Dinheiro a, int quantidade) => new(a.Valor * quantidade, a.Moeda);

    public override string ToString() => Moeda == Moeda.BRL ? Valor.ToString("C", new CultureInfo("pt-BR")) : $"{Valor:N2} {Moeda}";
}

A partir daí, um Isbn existente é válido: não há como construir um inválido, e o resto do código não precisa verificar de novo. Somar reais com euros é um erro detectado na hora, e não um total errado descoberto na contabilidade. O Isbn é uma record class, e não uma record struct, por um motivo sutil: toda struct tem um valor padrão, default(Isbn), que existiria com Valor nulo, contornando a validação. Para Dinheiro, o padrão (zero reais) é um valor legítimo, e a struct serve.

Objetos de valor exigem trabalho na infraestrutura: um conversor de valor no EF Core (HasConversion(i => i.Valor, s => Isbn.Criar(s))), como o conversor de centavos de "EF Core: Relacionamentos e Migrations", e um serializador no Mongo. É mais um exemplo de custo pago na borda para simplificar o centro.

Agregados e a fronteira da transação

Um agregado é um conjunto de objetos tratados como uma unidade de consistência, com uma raiz por onde toda alteração passa. Pedido é a raiz, e ItemPedido só existe dentro dele: ninguém altera a quantidade de um item diretamente, só pelo pedido, que garante que o total, o status e os itens continuam coerentes.

public sealed class Pedido : IGeraEventos
{
    private readonly List<ItemPedido> itens = [];
    private readonly List<IEventoDeDominio> eventos = [];

    public int Numero { get; private set; }
    public string ClienteId { get; }
    public StatusDoPedido Status { get; private set; } = StatusDoPedido.Registrado;
    public IReadOnlyList<ItemPedido> Itens => itens;
    public Dinheiro Total => itens.Aggregate(Dinheiro.Reais(0), (t, i) => t + i.Subtotal);

    public void Cancelar(string motivo)
    {
        if (Status is StatusDoPedido.Enviado or StatusDoPedido.Entregue)
            throw new RegraDeNegocioException("Pedido já enviado não pode ser cancelado; solicite devolução.");
        if (Status == StatusDoPedido.Cancelado) return;

        Status = StatusDoPedido.Cancelado;
        eventos.Add(new PedidoFoiCancelado(this, motivo));
    }

    // ...
}

A regra mais importante dos agregados, segundo Vaughn Vernon em Implementing Domain-Driven Design, é: uma transação altera um agregado só. Ela explica, em retrospecto, uma das decisões mais trabalhosas do curso. Produto (com o estoque) e Pedido são agregados diferentes. Registrar uma venda altera os dois, e por isso o ServicoDeVendas de "Um repositório, três bancos" precisou coordenar um update condicional no estoque e a inserção do pedido, com tentativas em caso de conflito. Uma solução "mais pura" seria reservar o estoque e confirmar o pedido em duas transações, ligadas por eventos, com consistência eventual. A Livraria escolheu a transação única entre dois agregados, porque ela vive num único banco, e porque vender o último exemplar duas vezes era o risco que mais importava. É uma exceção consciente, documentada e testada.

Quando os agregados vivem em serviços diferentes, a transação única deixa de existir, e os eventos de domínio de "Worker Services e mensageria com RabbitMQ", com o outbox, são o mecanismo de coordenação. O cancelamento de um pedido emite PedidoFoiCancelado, e um consumidor devolve os livros ao estoque, numa transação própria.

Contextos delimitados

A parte estratégica do DDD começa com a observação de que um modelo único para uma empresa inteira não existe. "Produto" significa uma coisa para o catálogo (título, autor, sinopse, capa), outra para o estoque (quantidade, localização, custo) e outra para o frete (peso, dimensões). Forçar um único Produto a servir aos três gera uma classe enorme, que todo mundo altera e ninguém entende.

Um contexto delimitado (bounded context) é a fronteira dentro da qual um modelo é consistente. A Livraria já tem alguns, mesmo sem ter dado o nome:

Contexto Onde vive Modelo de "livro"
Catálogo e vendas Domínio, API título, autor, preço, estoque disponível
Frete Livraria.Frete volume com peso e quantidade; nem sabe o que é um livro
Identidade Keycloak não há livros; há usuários, papéis e sessões
Jogo e ranking Biblioteca em Apuros, API "livro" é um objeto que cai; pontuação e recordes

A ligação entre contextos é feita por tradução, e não pelo compartilhamento do modelo. A API traduz o carrinho em volumes antes de chamar o frete. O frete, por sua vez, traduz as respostas das transportadoras (cada uma com seu formato, seus códigos de erro e sua ideia de prazo) no modelo dele. Uma camada que protege um modelo das idiossincrasias de outro sistema tem nome no DDD: camada anticorrupção. Ela é o motivo pelo qual uma transportadora que muda seu formato de resposta afeta só um adaptador dentro do serviço de frete, e nunca a API.

O que deixamos de fora

"Leve" significa também não adotar o que não se paga:

  • Repositório genérico (IRepositorio<T> com Obter, Listar, Adicionar para qualquer entidade). A Livraria tem repositórios por agregado, com as operações que o negócio precisa, e sem um IQueryable exposto que deixaria a aplicação montar consultas arbitrárias.
  • Uma camada de "serviços de domínio" para cada operação. As regras moram nas entidades e objetos de valor, e serviços existem só quando a operação envolve vários agregados, como a venda.
  • Um mediador entre endpoints e aplicação. Bibliotecas como o MediatR popularizaram o padrão de enviar "comandos" a "manipuladores". Na Livraria, um endpoint chama o serviço diretamente, e a indireção extra não resolveria nenhum problema real. Desde 2025, o MediatR e o AutoMapper passaram a ter licença comercial, o que levou muitas equipes a rever se precisavam deles, e várias descobriram que não.

CQRS sem cerimônia

A leitura e a escrita da Livraria já seguem caminhos diferentes, e isso também tem nome. CQRS (Command Query Responsibility Segregation) separa as operações que alteram estado das que consultam:

  • Escritas passam pelo domínio: carregar o agregado, chamar métodos que aplicam invariantes, salvar. O modelo rico existe para proteger as regras.
  • Leituras não precisam do domínio. ListarAsync com AsNoTracking e projeção direta para DTO, a busca com unaccent, o ranking no sorted set do Redis e o cache em três níveis de "Cache com Redis, HybridCache e output caching" leem de forma otimizada para cada tela, sem instanciar entidades.

Na versão mais ambiciosa, CQRS usa bancos diferentes para escrita e leitura, sincronizados por eventos, com o ranking do Redis sendo um exemplo pequeno disso. Na versão sem cerimônia, que é a da Livraria, é só a permissão para ler do jeito mais simples e rápido, sem passar pelo modelo de escrita.

Fatias verticais

A organização em camadas (Dominio, Aplicacao, Dados, Api) agrupa o código por tipo técnico. Uma funcionalidade nova, como o "avise-me quando chegar", toca todas as camadas: um endpoint na API, talvez um método no serviço, um armazenamento, um consumidor no worker. Quem procura "tudo sobre avisos" encontra o código espalhado.

A arquitetura em fatias verticais, formulada por Jimmy Bogard, agrupa por funcionalidade: uma pasta Funcionalidades/Avisos/ com o endpoint, o DTO, a validação e a lógica, juntos. As fatias compartilham o domínio e a infraestrutura, mas cada uma decide como implementar sua parte: umas com o modelo rico, outras com uma consulta direta.

As duas organizações convivem. A Livraria usa camadas entre projetos, para garantir a regra de dependência pelo compilador, e fatias dentro da API, onde os endpoints de "A API da Livraria" já estavam agrupados por recurso (EndpointsDeProdutos, EndpointsDePedidos). Uma boa pergunta para decidir onde algo vai é: quando esta funcionalidade mudar, o que mais vai mudar junto? O que muda junto deve morar junto.

Os padrões, com nome

Ao longo do curso, vários padrões apareceram resolvendo problemas concretos, sem que precisássemos de um catálogo. Com os nomes, eles viram vocabulário para conversar com outras pessoas:

Padrão Onde apareceu Problema resolvido
Repository "Um repositório, três bancos" isolar o domínio da persistência
Unit of Work o DbContext do EF Core agrupar alterações numa gravação
Strategy IArmazemDeArquivos (disco, S3), ICarrinhos (servidor, BFF) trocar o algoritmo sem mudar quem usa
Adapter a Principal do jogo, os clientes da API ligar interfaces incompatíveis
Decorator os DelegatingHandler de cache e token acrescentar comportamento em camadas
Observer eventos C#, sinais do Godot, SignalR avisar interessados sem conhecê-los
State a bibliotecária, o fluxo de telas comportamento que depende do estado
Object Pool livros, traças, poeira reaproveitar objetos caros
Outbox caixa de saída da venda, fila do caixa publicar eventos sem escrita dupla
Circuit Breaker resiliência dos clientes HTTP parar de insistir num serviço caído
Backend for Frontend o site com YARP uma API sob medida para o front-end, com segredos no servidor
Anti-Corruption Layer o serviço de frete proteger um modelo das idiossincrasias de outro
Result Resultado<T> do site tornar falhas esperadas explícitas no tipo

O livro Design Patterns (Gamma, Helm, Johnson e Vlissides, 1994) catalogou os primeiros, e Patterns of Enterprise Application Architecture, de Martin Fowler (2002), e Enterprise Integration Patterns, de Gregor Hohpe e Bobby Woolf (2003), os outros. O valor dos padrões não está em aplicá-los, e sim em reconhecer um problema já conhecido e saber, de antemão, as consequências da solução habitual.

Monólito modular antes de microsserviços

A Livraria tem vários processos (API, worker, frete, site), mas o núcleo, o domínio de catálogo e vendas, é um só, num único banco. É um monólito modular com alguns serviços satélites, e não uma arquitetura de microsserviços. A diferença é deliberada.

Microsserviços compram implantação independente e escala independente, e pagam com o que as Fases 6 e 7 mostraram em detalhe: rede entre tudo, consistência eventual, rastreamento distribuído, versões de contrato, deploy de muitas peças. O serviço de frete foi separado porque tinha motivos concretos: é chamado com prazo curto, fala com sistemas externos lentos e instáveis e tem um modelo próprio. Separar "catálogo" de "pedidos" em dois serviços, no tamanho atual da Livraria, só traria os custos.

A Lei de Conway (1968) observa que sistemas acabam espelhando a estrutura de comunicação das organizações que os constroem. Microsserviços fazem sentido quando há várias equipes que precisam trabalhar e implantar sem se coordenar a cada mudança. Uma equipe só, com um monólito bem dividido em módulos, com fronteiras testadas como as do teste de arquitetura acima, consegue separar um módulo em serviço no dia em que isso for necessário. O caminho inverso, juntar serviços que nunca deveriam ter sido separados, é bem mais difícil.

Registrando as decisões

Ao longo do curso, várias decisões foram tomadas com argumentos cuidadosos: o carrinho do site em WebAssembly e não em Server, o carrinho do aplicativo local e não compartilhado, a venda de balcão que pode deixar o estoque negativo, a transação única entre produto e pedido. Daqui a dois anos, alguém vai olhar o código e perguntar "por que isso foi feito assim?", e os argumentos estarão perdidos em conversas antigas.

Um ADR (Architecture Decision Record), no formato proposto por Michael Nygard em 2011, é um arquivo curto no repositório para cada decisão significativa:

# ADR 0014 — Venda de balcão registra mesmo com estoque insuficiente

## Status
Aceita (2026-09)

## Contexto
O caixa da loja física opera offline (fila local). Vendas podem chegar à API
depois que o estoque do sistema já foi consumido por vendas online. O livro,
porém, já foi entregue ao cliente no balcão.

## Decisão
O endpoint /v1/vendas-balcao registra a venda mesmo que o estoque fique negativo,
publica `estoque.divergente` e marca a venda para conferência.
A venda online continua com o update condicional, e nunca vende sem estoque.

## Consequências
+ O balcão nunca recusa uma venda que já aconteceu fisicamente.
− Estoque negativo passa a ser um estado possível e precisa ser tratado nos relatórios.
− A reconciliação é manual.

ADRs não são documentação exaustiva. São o registro do porquê, com as alternativas consideradas e as consequências aceitas. Ficam numa pasta docs/adr/, numerados, e nunca são editados depois de aceitos: uma decisão revista gera um ADR novo, que substitui o antigo e aponta para ele. O histórico de como o sistema pensou fica preservado.

Quanta arquitetura

A pergunta final é a mais difícil: quanto disso tudo um sistema precisa? A resposta honesta é proporcional ao problema e ao tempo de vida esperado.

  • Um script de migração que roda uma vez não precisa de camadas, e um arquivo com instruções de topo, como os que o .NET 10 permite rodar diretamente, é a arquitetura certa.
  • As regras do jogo, com uma classe e uma dúzia de testes, precisam só de estar separadas do motor.
  • O domínio de vendas, que vai viver anos e ser alterado por muitas mãos, justifica objetos de valor, agregados, testes de arquitetura e ADRs.

O erro mais comum não é arquitetura de menos, e sim arquitetura antes da hora: interfaces com uma única implementação que nunca terá outra, camadas que só repassam chamadas, abstrações para uma flexibilidade que ninguém pediu. O princípio YAGNI (You Aren't Gonna Need It) vale aqui. A Livraria criou IRepositorioProdutos quando a segunda implementação era iminente (o curso já tinha três bancos planejados), e não antes. Essa ordem, a necessidade primeiro e a abstração depois, é talvez a lição de arquitetura mais útil do curso.

A arquitetura da Livraria agora tem nomes: uma regra de dependência que aponta para o domínio e é verificada por testes, objetos de valor que tornam estados inválidos impossíveis, agregados que definem as fronteiras de consistência, contextos delimitados com tradução entre eles, leituras e escritas por caminhos diferentes, padrões reconhecíveis e decisões registradas com seus porquês.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escreva um teste de arquitetura que garanta que nenhum tipo do domínio tem setter público, e explique que falha ele encontraria hoje e se ela deve ser corrigida ou registrada como exceção.

Ver resposta

✓ Resposta: Com NetArchTest: Types.InAssembly(typeof(Pedido).Assembly).That().AreClasses().Should().BeImmutable().GetResult() verifica que as classes não têm setters públicos nem campos públicos mutáveis. Uma alternativa mais fina é percorrer os tipos por reflexão e verificar prop.SetMethod?.IsPublic != true em cada propriedade. Hoje ele provavelmente falharia em DTOs ou tipos auxiliares que acabaram no assembly do domínio, e possivelmente em propriedades com set público exigidas por algum mapeamento. Cada falha é uma decisão: se a propriedade pública existe só para a infraestrutura (um serializador que exige setter), a correção é mudar o mapeamento (construtores, campos privados no EF, class maps no Mongo) e manter o domínio fechado. Se existe por uma razão de domínio legítima (raro), o teste ganha uma lista explícita de exceções com um comentário e um ADR. O objetivo não é zero exceções, e sim nenhuma exceção acidental.

Exercício 2

Um colega propõe que ItemPedido tenha um repositório próprio, IRepositorioItensPedido, "para alterar a quantidade de um item sem carregar o pedido inteiro". Avalie a proposta à luz de agregados.

Ver resposta

✓ Resposta: ItemPedido faz parte do agregado Pedido, cuja raiz garante invariantes que envolvem todos os itens: o total, a proibição de alterar itens depois do envio, limites de quantidade por pedido. Um repositório de itens permitiria alterar um item sem passar pela raiz, e cada uma dessas regras poderia ser violada: mudar a quantidade de um item de um pedido já enviado, ou deixar o total gravado em desacordo com os itens. A regra dos agregados é que só a raiz tem repositório. O argumento de desempenho raramente se sustenta: um pedido tem poucos itens, e carregá-los é barato. Se o carregamento fosse de fato um problema (um agregado com milhares de filhos), isso seria um sinal de que o agregado está grande demais e deveria ser redesenhado, e não de que a raiz deve ser contornada.

Exercício 3

Hoje Produto tem título, autor, preço, estoque, gêneros e capa. Descreva como ele seria dividido entre os contextos "Catálogo", "Estoque" e "Frete", e o que liga as partes.

Ver resposta

✓ Resposta: Catálogo: Livro com ISBN, título, autor, sinopse, gêneros, capa e preço de venda, alterado pela área administrativa, com leitura intensa e escrita rara. Estoque: ItemDeEstoque com o identificador do produto, quantidade disponível, quantidade reservada, localização na loja e custo de aquisição, alterado a cada venda, com concorrência intensa e regras de reserva e reconciliação. Frete: Volume com peso e dimensões, derivado do produto e mantido pelo serviço de frete. O que liga as partes é o identificador do produto (ou o ISBN), compartilhado como referência, nunca o objeto. Mudanças atravessam contextos por eventos: livro.cadastrado cria o item de estoque com quantidade zero, estoque.alterado atualiza a disponibilidade exibida no catálogo (uma projeção de leitura). A divisão só se paga se as partes tiverem ritmos, equipes ou regras diferentes. Na Livraria atual, a separação entre catálogo e estoque já aparece nas regras do balcão e na concorrência, e seria a primeira a se justificar.

Exercício 4

Transforme a regra "o preço de um livro nunca pode ser zero ou negativo, e tem no máximo duas casas decimais" num objeto de valor. Onde a regra deixa de precisar ser verificada?

Ver resposta

✓ Resposta: public readonly record struct Preco { public Dinheiro Valor { get; } private Preco(Dinheiro v) => Valor = v; public static Preco Criar(decimal valor) => valor <= 0 || decimal.Round(valor, 2) != valor ? throw new RegraDeNegocioException("O preço deve ser positivo e ter no máximo duas casas decimais.") : new Preco(Dinheiro.Reais(valor)); }. Como default(Preco) seria um preço zero, a regra pode ser contornada com default, e por isso uma sealed record (classe) é mais segura, ou então o domínio precisa rejeitar default explicitamente nos pontos de entrada. Com o tipo, LivroImpresso passa a ter public Preco Preco { get; private set; }, e seus métodos não precisam mais verificar a regra: qualquer Preco existente é válido. A verificação acontece uma vez, na construção, na borda (o mapeamento do DTO, o conversor do EF, o serializador do Mongo). O domínio inteiro deixa de ter if (preco <= 0) espalhado.

Exercício 5

Escreva o ADR da decisão de "A API da Livraria" de usar ETag por hash da representação em vez de uma coluna de versão, incluindo a condição que levaria a revê-la.

Ver resposta

✓ Resposta: ADR 0007 — Concorrência otimista na API por ETag de hash da representação. Status: aceita (Fase 6). Contexto: a API precisa impedir atualizações perdidas entre requisições (If-Match/412) e funcionar igual nos três provedores de banco, sem mudança de esquema imediata. Decisão: o ETag é um hash SHA-256 truncado do JSON do ProdutoResposta. O PUT compara o If-Match com o hash do estado atual lido do banco. O estoque continua protegido pelo token de concorrência do EF e pelo update condicional. Consequências: + nenhuma migração, funciona igual em PostgreSQL, MySQL e MongoDB; + o ETag muda sempre que a representação muda. − Há uma janela entre a comparação e a escrita em que alterações de título e preço podem se sobrepor. − O ETag muda também quando só o formato da resposta muda (uma nova versão de contrato). Revisão: substituir por coluna de versão (ADR a escrever) se a edição concorrente de título e preço por vários administradores se tornar frequente, ou quando a área administrativa ganhar edição em massa, que aumenta a chance de sobreposição.

Comentários

Mais em Linguagem C#

Herança: Quando um Tipo É uma Versão Especializada de Outro
Herança: Quando um Tipo É uma Versão Especializada de Outro

Herança em C#: classe base e derivada, construtor com base, virtual e…

MongoDB Avançado: Agregações, Índices e a Escolha Entre Documento e Relacional
MongoDB Avançado: Agregações, Índices e a Escolha Entre Documento e Relacional

O MongoDB além do básico: pipeline de agregação em LINQ, $lookup, índices…

C# nos jogos: Unity, Godot, MonoGame e o game loop
C# nos jogos: Unity, Godot, MonoGame e o game loop

Como o C# chegou aos jogos pela Unity, pelo XNA e MonoGame e pelo Godot, a…