DDD na prática: Onion e CQRS
ResumoDDD tático encaixa naturalmente na Onion Architecture (Módulo 03) — e conversa muito bem com o CQRS, que você verá no Módulo 08: o domínio no centro, sem dependências; a aplicação orquestra casos de uso; a infraestrutura implementa persistência. Um repositório por raiz de agregado.
1. Objetivos de Aprendizagem#
- Posicionar entidades, VOs, agregados e repositórios nas camadas da Onion.
- Definir um repositório por raiz de agregado.
- Antever como DDD e CQRS se combinam: commands mutam via domínio; queries leem direto.
2. Pré-requisitos#
- Onion Architecture (Aula 17.1) e Repository/Service (Aula 17.3).
- Domínio rico (Aula 44.4).
A seção sobre CQRS é um preview do Módulo 08 (Tópicos Avançados) — você não precisa conhecê-lo ainda; ele volta lá em detalhe.
3. Conceito#
A Onion Architecture e o DDD são feitos um para o outro. As dependências apontam para dentro — o domínio não conhece ninguém:
- Domain (centro): entidades, value objects, agregados e as interfaces de repositório (
IProdutoRepository). Zero dependência de framework. É onde vivem as regras (Aula 44.4). - Application: casos de uso que orquestram o domínio — na prática, os handlers de command/query do CQRS (que você verá no Módulo 08). Dependem do domínio, não da infraestrutura.
- Infrastructure: implementações concretas —
ProdutoRepositorycom EF Core, mapeamento dos value objects (OwnsOne),DbContext. Depende do domínio (implementa as interfaces dele).
A regra um repositório por raiz de agregado vem daqui: repositórios carregam e salvam agregados inteiros pela raiz, nunca objetos internos soltos. Não existe IItemPedidoRepository — o item se salva através do Pedido.
Com CQRS, a divisão fica limpa:
- Commands (escrita) carregam o agregado, chamam um método de domínio (que garante a invariante) e salvam. Passam pelo modelo rico.
- Queries (leitura) podem pular o domínio e projetar direto para um DTO/
SELECTotimizado — não há invariante a proteger numa leitura.
4. Mão na Massa#
Domain — a interface do repositório mora aqui (perto de quem ela serve):
// Domain/Repositories/IProdutoRepository.cs
public interface IProdutoRepository
{
Task<Produto?> ObterAsync(Guid id, CancellationToken ct = default);
Task SalvarAsync(Produto produto, CancellationToken ct = default);
}
Application — um command handler (estilo MediatR, detalhado no Módulo 08) orquestra:
// Application/Produtos/ReajustarPreco.cs
public record ReajustarPrecoCommand(Guid ProdutoId, decimal Percentual) : IRequest;
public class ReajustarPrecoHandler : IRequestHandler<ReajustarPrecoCommand>
{
private readonly IProdutoRepository _repo;
public ReajustarPrecoHandler(IProdutoRepository repo) => _repo = repo;
public async Task Handle(ReajustarPrecoCommand cmd, CancellationToken ct)
{
var produto = await _repo.ObterAsync(cmd.ProdutoId, ct)
?? throw new KeyNotFoundException();
produto.Reajustar(cmd.Percentual); // regra no domínio (Aula 44.4)
await _repo.SalvarAsync(produto, ct);
}
}
Infrastructure — EF Core implementa a interface e mapeia o value object:
// Infrastructure/Persistence/ProdutoRepository.cs
public class ProdutoRepository : IProdutoRepository
{
private readonly CatalogDbContext _db;
public ProdutoRepository(CatalogDbContext db) => _db = db;
public Task<Produto?> ObterAsync(Guid id, CancellationToken ct = default) =>
_db.Produtos.FirstOrDefaultAsync(p => p.Id == id, ct);
public async Task SalvarAsync(Produto produto, CancellationToken ct = default)
{
_db.Produtos.Update(produto);
await _db.SaveChangesAsync(ct);
}
}
// Value object Dinheiro mapeado como owned type
public class CatalogDbContext : DbContext
{
public DbSet<Produto> Produtos => Set<Produto>();
protected override void OnModelCreating(ModelBuilder b) =>
b.Entity<Produto>().OwnsOne(p => p.Preco); // Preco.Valor / Preco.Moeda
}
A leitura, por CQRS, não precisa do agregado — projeta direto:
// Application/Produtos/ObterCatalogo.cs (query)
var itens = await _db.Produtos
.Select(p => new ProdutoDto(p.Id, p.Nome, p.Preco.Valor, p.Preco.Moeda))
.ToListAsync(ct);
5. Exemplo Completo#
O fluxo de uma escrita: Controller → envia ReajustarPrecoCommand (MediatR) → Handler carrega o Produto pelo IProdutoRepository → chama produto.Reajustar() (invariante garantida no domínio) → repositório salva o agregado. A regra está no centro; as bordas (HTTP, EF) só entram e saem. Comparado à Aula 17.3, o Service deixa de conter regra e vira orquestração fina.
6. Boas Práticas e Armadilhas#
| Faça | Evite |
|---|---|
| Interface de repositório no Domain; implementação na Infra | DbContext/EF vazando para o domínio |
| Um repositório por raiz de agregado | Repositório para entidade interna (ItemPedido) |
| Query lendo direto para DTO | Carregar o agregado só para ler e devolver |
Atençãonão transforme o repositório num CRUD genérico
IRepository<T>que expõeIQueryable. Isso fura a fronteira do agregado e deixa a query montar estados que o domínio nunca permitiria.
7. Segurança e Produção#
- Com o domínio isolado, as regras não dependem de HTTP nem de banco: dá para testá-las com testes de unidade rápidos (Módulo 15), sem infraestrutura. Combine com concorrência otimista (Aula 44.3) na raiz do agregado.
8. Exercícios#
- Fácil: desenhe as três camadas e coloque
Produto,IProdutoRepository,ProdutoRepositorye o handler em cada uma. - Médio: escreva um
AdicionarItemCommand+ handler para o agregadoPedido(Aula 44.3). - Desafio: implemente uma query de catálogo paginada (Aula 30.1) que não passe pelo agregado e explique por quê isso é seguro.
9. Resumo#
DDD encaixa na Onion (domínio no centro, aplicação orquestra, infra implementa) e no CQRS (commands via domínio, queries diretas). Um repositório por raiz de agregado mantém as fronteiras — e as regras — no lugar certo.
10. Próximos Passos#
Você tem o mapa do DDD tático aplicado ao projeto. Reforce com os testes do Módulo 15 e, se o domínio crescer, estude domain events para coordenar mudanças entre agregados.
11. Referências#
- Microsoft Learn — Padrões DDD e CQRS em microsserviços
- Microsoft Learn — Design da camada de persistência (repositórios)