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/)

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.