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
- Martin Fowler — Repository Pattern — Definição original do padrão e seus casos de uso
- Microsoft Docs — Repository Pattern in ASP.NET Core — Guia oficial sobre implementação prática com Entity Framework
- Vladimir Khorikov — Don't Use Repository Pattern with Entity Framework — Análise crítica sobre quando o padrão é desnecessário
- Hadi Hariri — Repository Pattern: Evil or Necessary Evil? — Discussão sobre os prós e contras do padrão
- Steve Smith — Repository Pattern: Why You Should Use It — Defesa do padrão com exemplos práticos de implementação enxuta
- Entity Framework Documentation — DbContext as Repository — Explicação de como o DbContext já implementa os padrões Repository e Unit of Work
- Refactoring Guru — Repository Pattern — Explicação visual e exemplos em múltiplas linguagens