Repositories no DDD
1. Fundamentos do Repository Pattern no DDD
1.1. Definição e propósito: abstração de persistência para o domínio
O Repository Pattern é um dos pilares do Domain-Driven Design (DDD) proposto por Eric Evans. Seu propósito fundamental é fornecer uma abstração de persistência que permita ao domínio operar sem conhecer os detalhes de como os dados são armazenados e recuperados. O Repository atua como uma "coleção em memória" de objetos de domínio, escondendo a complexidade do armazenamento subjacente.
// Interface de Repository no domínio
public interface IClienteRepository
{
Cliente ObterPorId(Guid id);
void Adicionar(Cliente cliente);
void Remover(Cliente cliente);
}
1.2. Diferença entre Repository e DAO (Data Access Object)
Enquanto o DAO foca no acesso a dados como entidades de banco relacional, o Repository opera com objetos de domínio e aggregates. O Repository retorna objetos de domínio completos e consistentes, enquanto o DAO frequentemente retorna estruturas de dados anêmicas ou registros isolados.
// DAO típico (foco em dados)
public interface ClienteDAO
{
DataTable ObterClientesAtivos();
void InserirCliente(DataRow row);
}
// Repository (foco em domínio)
public interface IClienteRepository
{
Cliente ObterPorId(Guid id);
void Adicionar(Cliente cliente);
}
1.3. O Repository como parte do domínio (não da infraestrutura)
No DDD, a interface do Repository pertence à camada de Domínio, não à infraestrutura. Isso permite que o domínio defina contratos de persistência sem depender de implementações concretas.
// Camada de Domínio
namespace Dominio.Repositories
{
public interface IPedidoRepository
{
Pedido ObterPorId(Guid id);
void Adicionar(Pedido pedido);
}
}
// Camada de Infraestrutura
namespace Infraestrutura.Repositories
{
public class PedidoRepository : IPedidoRepository
{
private readonly DbContext _context;
public Pedido ObterPorId(Guid id)
{
return _context.Pedidos
.Include(p => p.Itens)
.FirstOrDefault(p => p.Id == id);
}
public void Adicionar(Pedido pedido)
{
_context.Pedidos.Add(pedido);
}
}
}
2. Responsabilidades e Limites do Repository
2.1. Gerenciamento do ciclo de vida de Aggregates
O Repository gerencia o ciclo de vida completo dos aggregates: recuperação, adição e remoção. Ele garante que, ao recuperar um aggregate, todas as suas entidades e value objects internos sejam carregados corretamente.
public interface IPedidoRepository
{
Pedido ObterPorId(Guid id); // Retorna aggregate completo com itens
void Adicionar(Pedido pedido); // Persiste aggregate e seus itens
void Remover(Pedido pedido); // Remove aggregate e itens associados
}
2.2. Operações permitidas: Add, Remove, Find (não Update explícito)
No DDD, não existe método Update. As alterações são detectadas pelo Unit of Work ou pelo mecanismo de tracking do ORM. O Repository apenas adiciona, remove e localiza aggregates.
public interface IClienteRepository
{
Cliente ObterPorId(Guid id); // Find
void Adicionar(Cliente cliente); // Add
void Remover(Cliente cliente); // Remove
// NÃO existe: void Atualizar(Cliente cliente);
}
2.3. O que NÃO deve estar no Repository
Consultas complexas, lógica de negócio e gerenciamento de transações não pertencem ao Repository. O Repository deve ser simples e focado em persistência.
// ERRADO: Repository com lógica de negócio
public class PedidoRepository : IPedidoRepository
{
public void AplicarDesconto(Guid pedidoId, decimal percentual)
{
var pedido = ObterPorId(pedidoId);
if (pedido.ValorTotal > 1000)
pedido.AplicarDesconto(percentual); // Lógica de negócio aqui!
}
}
// CERTO: Lógica de negócio no domínio
public class Pedido
{
public void AplicarDesconto(decimal percentual)
{
if (ValorTotal > 1000)
ValorTotal -= ValorTotal * percentual / 100;
}
}
3. Repository e Aggregates: a Unidade de Consistência
3.1. Um Repository por Aggregate Root
Cada Aggregate Root possui seu próprio Repository. Não existem repositories para entidades internas do aggregate.
// Aggregate Root: Pedido
public class Pedido
{
public Guid Id { get; private set; }
private List<ItemPedido> _itens;
public IReadOnlyCollection<ItemPedido> Itens => _itens.AsReadOnly();
}
// Repository correto
public interface IPedidoRepository
{
Pedido ObterPorId(Guid id);
}
// ERRADO: Repository para entidade interna
public interface IItemPedidoRepository { } // Não deve existir!
3.2. Persistência completa do Aggregate
Ao persistir um aggregate, todo ele deve ser salvo como uma unidade. Não é permitido salvar apenas partes do aggregate.
public class PedidoRepository : IPedidoRepository
{
public void Adicionar(Pedido pedido)
{
// Persiste o pedido e todos os seus itens em uma única operação
_context.Pedidos.Add(pedido);
_context.SaveChanges(); // Garante consistência
}
}
3.3. Implicações: carregamento lazy vs. eager e performance
O carregamento lazy pode quebrar a consistência do aggregate e causar problemas de performance (N+1). Prefira carregamento eager para garantir que o aggregate seja completamente hidratado.
public Pedido ObterPorId(Guid id)
{
return _context.Pedidos
.Include(p => p.Itens) // Eager loading
.ThenInclude(i => i.Produto) // Carrega também o produto
.FirstOrDefault(p => p.Id == id);
}
4. Interface vs. Implementação: Separação de Camadas
4.1. Interface no domínio (camada Domain)
A interface do Repository é definida na camada de Domínio, pois representa um contrato que o domínio exige.
namespace MeuDominio.Repositories
{
public interface IProdutoRepository
{
Produto ObterPorId(Guid id);
void Adicionar(Produto produto);
void Remover(Produto produto);
}
}
4.2. Implementação na infraestrutura (camada Infrastructure)
A implementação concreta reside na camada de Infraestrutura, com acesso a bancos de dados, ORMs ou outras tecnologias de persistência.
namespace MeuInfraestrutura.Repositories
{
public class ProdutoRepository : IProdutoRepository
{
private readonly MeuDbContext _context;
public ProdutoRepository(MeuDbContext context)
{
_context = context;
}
public Produto ObterPorId(Guid id)
{
return _context.Produtos.Find(id);
}
public void Adicionar(Produto produto)
{
_context.Produtos.Add(produto);
}
public void Remover(Produto produto)
{
_context.Produtos.Remove(produto);
}
}
}
4.3. Injeção de dependência e inversão de controle
A inversão de controle permite que o domínio utilize o Repository sem conhecer sua implementação concreta.
// Configuração no Startup ou Program.cs
services.AddScoped<IProdutoRepository, ProdutoRepository>();
services.AddScoped<IPedidoRepository, PedidoRepository>();
// Uso no domínio
public class CriacaoPedidoService
{
private readonly IPedidoRepository _pedidoRepository;
private readonly IProdutoRepository _produtoRepository;
public CriacaoPedidoService(
IPedidoRepository pedidoRepository,
IProdutoRepository produtoRepository)
{
_pedidoRepository = pedidoRepository;
_produtoRepository = produtoRepository;
}
}
5. Padrões de Implementação de Repositories
5.1. Repository Genérico vs. Repository Específico
Repository genérico reduz código repetitivo, mas pode esconder particularidades de cada aggregate. Repository específico oferece mais controle.
// Genérico
public interface IRepository<T> where T : AggregateRoot
{
T ObterPorId(Guid id);
void Adicionar(T aggregate);
void Remover(T aggregate);
}
// Específico
public interface IPedidoRepository
{
Pedido ObterPorId(Guid id);
void Adicionar(Pedido pedido);
void Remover(Pedido pedido);
IEnumerable<Pedido> ObterPorCliente(Guid clienteId); // Específico
}
5.2. Implementação com ORM (Entity Framework, Hibernate)
ORMs facilitam a implementação de Repositories, mas é preciso cuidado para não vazar detalhes do ORM para o domínio.
public class PedidoRepository : IPedidoRepository
{
private readonly DbContext _context;
public Pedido ObterPorId(Guid id)
{
return _context.Set<Pedido>()
.Include(p => p.Itens)
.AsNoTracking() // Evita tracking desnecessário para leitura
.FirstOrDefault(p => p.Id == id);
}
public void Adicionar(Pedido pedido)
{
_context.Set<Pedido>().Add(pedido);
}
}
5.3. Implementação com armazenamento não relacional (NoSQL, event store)
Para NoSQL, o Repository pode serializar o aggregate inteiro como um documento.
public class PedidoRepositoryNoSql : IPedidoRepository
{
private readonly IMongoCollection<Pedido> _collection;
public Pedido ObterPorId(Guid id)
{
return _collection.Find(p => p.Id == id).FirstOrDefault();
}
public void Adicionar(Pedido pedido)
{
_collection.InsertOne(pedido); // Serializa o aggregate completo
}
}
6. Consultas no Repository: Limitações e Alternativas
6.1. Métodos de busca por identidade e critérios simples
O Repository deve expor métodos de busca limitados a critérios simples e por identidade.
public interface IProdutoRepository
{
Produto ObterPorId(Guid id);
IEnumerable<Produto> ObterPorCategoria(string categoria);
IEnumerable<Produto> ObterAtivos();
}
6.2. Quando usar Specification Pattern para consultas complexas
O Specification Pattern permite encapsular consultas complexas de forma reutilizável e combinável.
public interface ISpecification<T>
{
Expression<Func<T, bool>> ToExpression();
}
public class ProdutosComEstoqueBaixoSpecification : ISpecification<Produto>
{
private readonly int _limite;
public ProdutosComEstoqueBaixoSpecification(int limite)
{
_limite = limite;
}
public Expression<Func<Produto, bool>> ToExpression()
{
return p => p.QuantidadeEstoque < _limite;
}
}
public interface IProdutoRepository
{
IEnumerable<Produto> Find(ISpecification<Produto> specification);
}
6.3. Relação com CQRS: separação de queries de leitura
Em CQRS, o Repository é usado apenas para comandos (escrita). Consultas complexas são tratadas por queries separadas.
// CQRS: Repository para comandos
public interface IPedidoCommandRepository
{
void Adicionar(Pedido pedido);
Pedido ObterPorId(Guid id);
}
// Query separada para leitura
public interface IPedidoQuery
{
PedidoDTO ObterResumoPedido(Guid id);
IEnumerable<PedidoDTO> ObterPedidosDoCliente(Guid clienteId);
}
7. Transações, UoW e Consistência com Repositories
7.1. Unit of Work: coordenando múltiplos Repositories
O Unit of Work coordena múltiplos Repositories em uma única transação.
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
// Uso coordenado
public class TransferenciaService
{
private readonly IContaRepository _contas;
private readonly ITransacaoRepository _transacoes;
private readonly IUnitOfWork _uow;
public async Task Transferir(Guid origemId, Guid destinoId, decimal valor)
{
await _uow.BeginTransactionAsync();
try
{
var origem = _contas.ObterPorId(origemId);
var destino = _contas.ObterPorId(destinoId);
origem.Debitar(valor);
destino.Creditar(valor);
_transacoes.Adicionar(new Transacao(origemId, destinoId, valor));
await _uow.SaveChangesAsync();
await _uow.CommitTransactionAsync();
}
catch
{
await _uow.RollbackTransactionAsync();
throw;
}
}
}
7.2. Transações implícitas vs. explícitas no DDD
Transações implícitas são gerenciadas pelo ORM. Explícitas dão mais controle, mas podem vazar detalhes de infraestrutura.
// Implícita (ORM gerencia)
public void Adicionar(Pedido pedido)
{
_context.Pedidos.Add(pedido);
_context.SaveChanges(); // Transação implícita
}
// Explícita (controle manual)
public void AdicionarComTransacao(Pedido pedido)
{
using var transaction = _context.Database.BeginTransaction();
try
{
_context.Pedidos.Add(pedido);
_context.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
7.3. Domain Events e persistência: garantindo atomicidade
Domain Events devem ser publicados após a persistência bem-sucedida para garantir atomicidade.
public class PedidoService
{
private readonly IPedidoRepository _repository;
private readonly IUnitOfWork _uow;
private readonly IEventPublisher _eventPublisher;
public async Task CriarPedido(Pedido pedido)
{
_repository.Adicionar(pedido);
await _uow.SaveChangesAsync();
// Publica eventos APÓS a persistência
foreach (var evento in pedido.DomainEvents)
{
await _eventPublisher.PublishAsync(evento);
}
}
}
8. Anti-patterns e Armadilhas Comuns
8.1. Repository como "God Class" com muitos métodos
Repository com dezenas de métodos de consulta viola o princípio da responsabilidade única.
// ERRADO: God Class
public interface IProdutoRepository
{
Produto ObterPorId(Guid id);
IEnumerable<Produto> ObterPorNome(string nome);
IEnumerable<Produto> ObterPorCategoria(string cat);
IEnumerable<Produto> ObterPorPrecoMinimo(decimal min);
IEnumerable<Produto> ObterPorPrecoMaximo(decimal max);
IEnumerable<Produto> ObterPorEstoqueMinimo(int min);
// ... mais 20 métodos
}
// CERTO: Especificações ou queries separadas
public interface IProdutoRepository
{
Produto ObterPorId(Guid id);
IEnumerable<Produto> Find(ISpecification<Produto> spec);
}
8.2. Vazar detalhes de infraestrutura para o domínio
O domínio não deve conhecer DbContext, conexões, ou detalhes de ORM.
// ERRADO: Domínio dependente de infraestrutura
public class PedidoService
{
private readonly DbContext _context; // Vazamento!
public void Processar(Guid id)
{
var pedido = _context.Pedidos.Find(id); // Domínio acessa ORM
}
}
// CERTO: Domínio usa apenas interfaces
public class PedidoService
{
private readonly IPedidoRepository _repository; // Apenas interface
public void Processar(Guid id)
{
var pedido = _repository.ObterPorId(id);
}
}
8.3. Repositories anêmicos e violação do Aggregate boundary
Repository que persiste entidades internas do aggregate quebra o boundary.
// ERRADO: Repository para entidade interna
public interface IItemPedidoRepository
{
void AdicionarItem(ItemPedido item); // Violação do aggregate
}
// CERTO: Tudo passa pelo aggregate root
public interface IPedidoRepository
{
Pedido ObterPorId(Guid id); // Retorna aggregate completo
void Adicionar(Pedido pedido); // Persiste tudo
}
Referências
- [Domain-Driven Design: Tackling Complexity in the Heart of Software (Eric Evans)](https://www.domainlanguage
.com/ddd/)
- Implementing Domain-Driven Design (Vaughn Vernon)
- Patterns, Principles, and Practices of Domain-Driven Design (Scott Millett & Nick Tune)
- Martin Fowler: Repository Pattern
- Microsoft: Repository Pattern in DDD
Considerações Finais
O Repository Pattern no DDD é muito mais que uma simples abstração de banco de dados. Ele representa o contrato entre o domínio e a infraestrutura, garantindo que a complexidade do armazenamento não contamine o coração do sistema. Quando bem implementado, permite que o time de domínio foque nas regras de negócio enquanto a infraestrutura cuida dos detalhes técnicos.
A chave para o sucesso está em respeitar os limites do Aggregate, manter a interface limpa e enxuta, e utilizar padrões complementares como Specification e CQRS quando as consultas se tornarem complexas. Lembre-se: o Repository existe para servir o domínio, não para ser uma camada genérica de acesso a dados.