Padrão Repository: como implementar sem criar abstração inútil

1. O problema que o Repository realmente resolve

O padrão Repository existe para isolar a lógica de acesso a dados do restante da aplicação, permitindo que o domínio permaneça puro e testável. O problema é que muitos desenvolvedores transformam essa intenção nobre em uma camada de abstração genérica que não agrega valor real.

O Repository resolve um problema específico: quando sua lógica de negócio precisa interagir com o armazenamento de dados sem conhecer os detalhes de implementação. Isso é valioso em cenários como:

  • Troca de banco de dados (SQL para NoSQL)
  • Testes unitários com mocking
  • Múltiplas fontes de dados simultâneas

O erro comum é criar uma interface para cada entidade automaticamente, mesmo quando o sistema nunca trocará de banco e os testes podem ser feitos com o banco real.

2. Anatomia de um Repository enxuto

Um Repository bem projetado deve expor apenas operações que fazem sentido para o domínio. Métodos essenciais:

public class UserRepository
{
    private readonly AppDbContext _context;

    public UserRepository(AppDbContext context)
    {
        _context = context;
    }

    public User? FindById(Guid id)
    {
        return _context.Users.Find(id);
    }

    public void Save(User user)
    {
        if (user.Id == Guid.Empty)
            _context.Users.Add(user);
        else
            _context.Users.Update(user);
    }

    public void Delete(User user)
    {
        _context.Users.Remove(user);
    }
}

Evite criar métodos genéricos como FindAll(), FindByCondition(Expression<Func<T, bool>> predicate) — isso transforma seu Repository em um mini-ORM e anula o propósito do padrão.

3. A armadilha da abstração genérica

A interface IRepository<T> é um dos anti-padrões mais difundidos. Ela promete reuso, mas na prática:

public interface IRepository<T>
{
    T FindById(Guid id);
    void Save(T entity);
    void Delete(T entity);
}

Por que é inútil na maioria dos casos:

  • Cada entidade tem necessidades diferentes de persistência
  • Você acaba adicionando métodos que só algumas entidades usam
  • A troca de implementação nunca acontece (quem troca de ORM no meio do projeto?)
  • O custo de manutenção supera o benefício

Se você tem apenas uma implementação concreta e nunca precisou trocá-la, a interface é abstração por dogma, não por necessidade.

4. Repository vs. ORM: quem faz o quê?

O ORM (Entity Framework, Dapper, NHibernate) gerencia a persistência: conexões, queries SQL, tracking de mudanças. O Repository gerencia o domínio: regras de negócio que envolvem acesso a dados.

Quando usar o ORM diretamente:
- CRUD simples sem lógica de domínio
- Operações puramente de leitura (relatórios, dashboards)
- Protótipos e MVPs

Quando criar Repository:
- Regras de negócio que exigem consistência entre múltiplas entidades
- Necessidade de testar a lógica sem o banco real
- Domínio complexo com validações que dependem do estado persistido

Exemplo de uso direto do Entity Framework como Repository nativo:

public class UserService
{
    private readonly AppDbContext _context;

    public void CreateUser(string name, string email)
    {
        var user = new User(name, email);
        _context.Users.Add(user);
        _context.SaveChanges();
    }
}

Aqui não há necessidade de encapsular o DbContext, pois a operação é simples e não envolve regras de domínio complexas.

5. Implementação prática sem over-engineering

Comece com a classe concreta, sem interface. Adicione a interface apenas quando surgir uma real necessidade (segunda implementação, teste com mock, etc.).

// Comece assim
public class OrderRepository
{
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }

    public Order? FindById(Guid id)
    {
        return _context.Orders
            .Include(o => o.Items)
            .FirstOrDefault(o => o.Id == id);
    }

    public void Save(Order order)
    {
        // Lógica de negócio: validar status antes de salvar
        if (order.Status == OrderStatus.Cancelled && order.Items.Any())
            throw new DomainException("Cannot cancel order with items");

        _context.Orders.Update(order);
    }
}

// Injeção de dependência direta
services.AddScoped<OrderRepository>();

Para testes, use o banco real em memória (SQLite) ou um container Docker. O mocking só é necessário quando você precisa isolar o Repository de outras dependências.

6. Repository como fronteira do domínio

O Repository deve ser a fronteira entre o domínio e a persistência. Mantenha regras de negócio dentro das entidades e serviços de domínio, não no Repository.

Consultas complexas: crie métodos específicos

public class PaymentRepository
{
    public IEnumerable<Payment> FindOverduePayments(DateTime referenceDate)
    {
        return _context.Payments
            .Where(p => p.DueDate < referenceDate && p.Status == PaymentStatus.Pending)
            .ToList();
    }

    public Payment? FindByTransactionId(string transactionId)
    {
        return _context.Payments
            .FirstOrDefault(p => p.TransactionId == transactionId);
    }
}

Evite vazar conceitos de persistência: não retorne IQueryable, não exponha DbContext, não permita que o chamador construa queries.

7. Quando abrir mão do padrão

O padrão Repository não é obrigatório. Abra mão dele quando:

  • Aplicações CRUD simples: sem lógica de domínio, apenas Create/Read/Update/Delete
  • ORM já fornece abstração suficiente: Entity Framework DbContext já é um Unit of Work + Repository
  • Custo de manutenção supera o benefício: se você tem 50 repositórios e 48 são delegados diretos para o ORM, você está pagando um custo desnecessário

A decisão deve ser baseada em necessidade real, não em dogma arquitetural.

8. Conclusão: o Repository que você realmente precisa

Checklist para decidir se o padrão é necessário:

  • [ ] A lógica de negócio precisa consultar/alterar dados persistentes?
  • [ ] Existe a possibilidade real de trocar a fonte de dados?
  • [ ] Os testes unitários precisam isolar o acesso a dados?
  • [ ] O domínio tem regras que dependem de múltiplas consultas?

Se a maioria das respostas for "sim", implemente um Repository enxuto, específico para o domínio, sem abstrações genéricas desnecessárias.

Resumo das boas práticas:
- Métodos específicos para operações de negócio
- Sem IRepository<T> genérico
- Comece concreto, adicione interface só quando necessário
- Mantenha regras de negócio fora do Repository
- Use o ORM diretamente para CRUD simples

O equilíbrio está em abstrair o suficiente para isolar o domínio, mas não tanto a ponto de criar uma camada que apenas replica o ORM com nomes diferentes.

Referências