Arquitetura de Software em C# no Mundo Real: Capítulo 7 – Repositórios que respeitam o domínio (e evitam vazamento de infra)
Essa série de artigos nasceu da frustração — e da prática.
Se você já tentou organizar um sistema .NET complexo, sabe que não é fácil: cada time tem uma visão diferente, os tutoriais se contradizem, e os “padrões” muitas vezes atrapalham mais do que ajudam. Entre um GenericRepository<T> e um PaymentServiceHandlerProcessorFactory, fica difícil saber o que realmente importa.
Aqui, a proposta é simples: falar sobre arquitetura real, com um olhar crítico, direto e sem medo de questionar modinhas. Vamos explorar estilos, padrões, anti-padrões e armadilhas comuns que se acumulam ao longo dos anos — e mostrar alternativas que funcionam fora do slide de apresentação.
Essa série é para desenvolvedores que já sujaram as mãos. Que já herdaram legado, quebraram produção, e tentaram explicar DDD em reunião de grooming.
Sem fórmulas mágicas. Sem purismo. Só código, contexto e experiência real.
Índice da Série
Capítulo 1 – Os caminhos que a arquitetura pode tomar
Um panorama direto e sem rodeios sobre Transaction Script, Modelo Anêmico, Modelo Rico e DDD.
Capítulo 2 – O modelo anêmico vai te trair (e como saber quando fugir dele)
Quando usar um modelo anêmico parece mais simples, mas te afunda em bugs e complexidade acidental.
Capítulo 3 – A falsa promessa do “service pattern” genérico
AccountService,UserManager,FooHandler: nomes que escondem baixa coesão e design acoplado.
Capítulo 4 – Entidades ricas: encapsulamento é mais que private set;
Como construir entidades com comportamento real e proteger sua integridade.
Capítulo 5 – A camada de aplicação não é um lixo eletrônico
Application Services devem orquestrar, não conter regras de negócio nem virar dumping ground de dependências.
Capítulo 6 – Use Cases, Command Handlers e MediatR: o que separa clareza de confusão
Quando aplicar CQRS traz benefícios reais — e quando só adiciona complexidade desnecessária.
Capítulo 7 – Repositórios que respeitam o domínio (e evitam vazamento de infra)
Um repositório não é só um wrapper de
DbContext. É um contrato do domínio com a persistência.
Capítulo 8 – Domínio expressivo: Value Objects, Invariantes e Eventos de Domínio
Modelando regras com segurança e intenção. O domínio deve falar por si.
Capítulo 9 – Dividindo para conquistar: a importância dos Bounded Contexts
DDD real exige saber onde termina um modelo e começa outro. E como manter essa separação.
Capítulo 10 – Anti-patterns arquiteturais que todo dev .NET já viu (ou cometeu)
Um olhar honesto sobre os maiores pecados que já cometemos em nome da “arquitetura”.
Capítulo 11 – Mitos e Más Interpretações na Arquitetura .NET
Por que MVC não é arquitetura, repositório não é para toda entidade, padrão não é enfeite, e SOLID não é carta branca pra travar refatoração.
Capítulo 7 – Repositórios que respeitam o domínio (e evitam vazamento de infra)
Se tem um padrão que apanha muito na prática é o tal do Repository.
Ele aparece em toda arquitetura “DDD-like”, geralmente como um IRepository<T> com meia dúzia de métodos (Add, Update, Delete, GetById, GetAll).
Mas vamos ser honestos: isso é um DAL com outro nome.
Se o seu repositório é só um wrapper genérico em cima do Entity Framework, ele não está servindo ao domínio. Está servindo à infraestrutura.
E pior: está vazando detalhes da persistência para fora da camada que deveria estar protegida.
O problema do GenericRepository<T>
Essa abstração parece útil. Reaproveitável. DRY.
Mas o custo disso é altíssimo.
Imagine um GenericRepository<Trade> com método FindByCondition(Expression<Func<Trade,bool>> condition).
O que impede o application service de escrever uma query diretamente ali dentro, baseada em EF Core?
Nada.
O domínio passa a depender de expressões de LINQ, tipagem de EF, DbSet, AsNoTracking, Include, etc.
Resultado?
Você abstraiu o DbContext, mas espalhou EF para o domínio inteiro.
Repositório é um contrato do domínio, não um DSL de banco
Um bom repositório tem uma interface específica para o agregado que representa.
E a assinatura dos métodos fala a linguagem do domínio.
Veja o contraste:
// Interface genérica (anti-padrão)
public interface IRepository<T>
{
Task<T?> GetByIdAsync(Guid id);
Task<List<T>> FindByCondition(Expression<Func<T, bool>> condition);
}
// Interface rica e explícita (preferível)
public interface ITradeRepository
{
Task<Trade?> GetPendingByIdAsync(Guid tradeId);
Task<List<Trade>> GetByFirmAndStatus(string firm, TradeStatus status);
void Add(Trade trade);
}
Percebe a diferença?
O repositório agora conhece o negócio. Ele existe para atender casos de uso específicos.
E com isso, você ganha:
- Intellisense mais útil
- Testes mais fáceis (mock de métodos claros)
- Menos vazamento de infra
- Evolução mais segura (você não quebra tudo ao mudar um campo no banco)
Repositório é para agregados, não para entidades avulsas
No DDD, o repositório representa um Aggregate Root.
Isso quer dizer:
Você não precisa de repositório para tudo.
Você não precisa de um CustomerRepository, AddressRepository, PhoneRepository...
Você precisa de CustomerRepository se Customer é um Aggregate Root que contém Address e Phone.
A operação é sempre no agregado inteiro. Nunca em pedaços.
var customer = await _customerRepository.GetByEmail(email);
customer.UpdatePhone(newPhone);
await _unitOfWork.Commit();
Isso é o modelo funcionando como o DDD propõe.
Como testar repositórios?
Você pode manter o repositório com EF por baixo. Mas:
- Evite deixar ele exposto no domínio (ex: não injete
_dbContextem handler) - Escreva testes unitários para os métodos do repositório (mockando o DbContext ou usando EF in-memory)
- Escreva testes de integração para garantir que as queries refletem o modelo real
Mas não caia na armadilha de mockar LINQ com
Expression<Func<T,bool>>.
Teste as queries de verdade ou mantenha-as como privadas no repositório.
Dica prática: agrupe queries em ReadModels quando fizer sentido
Nem tudo precisa ir para o repositório do domínio.
Se sua query serve só para exibir dados na tela (ex: um dashboard), crie um ITradeReadModel separado.
Nele, você pode usar Dapper, LINQ, joins — o que quiser — sem poluir o modelo de domínio.
É o espírito do CQRS:
- O domínio cuida da escrita,
- Os ReadModels cuidam da leitura.
Conclusão
Repositórios não são para reutilização genérica.
Eles são contratos do domínio com a persistência, e devem falar a linguagem da sua aplicação.
Ao projetar bem o repositório, você:
- Protege o domínio de detalhes de infra
- Ganha confiança ao refatorar
- Reduz o acoplamento implícito
- E prepara o terreno para evoluir sem dor
Próximo capítulo:
E se estamos falando de proteger o modelo, então precisamos falar de um dos maiores aliados nessa missão: os Value Objects.