04 - Repositório (Repository)
video: https://youtu.be/... no frontmatter da nota.Repositório (Repository)
O repositório é uma abstração que expõe o acesso a agregados persistentes como se fossem uma
coleção em memória, oferecendo operações de adicionar, buscar e remover com semântica de domínio.
Entre o modelo rico e o banco de dados existe uma diferença irreconciliável de forma: o domínio
trabalha com objetos que protegem invariantes, e o banco trabalha com linhas e junções. O
repositório é a fronteira que absorve essa tradução, permitindo que o caso de uso pergunte "me dê o
pedido 42" sem saber se vem de Postgres, de cache ou de um serviço externo. Em produção, essa
abstração sustenta três necessidades contínuas: substituir a implementação de persistência sem
tocar no domínio, testar casos de uso sem banco e concentrar consultas complexas em um único lugar
com nome de negócio.
O problema que resolve
Sem a abstração, consultas e updates se espalham pelo código em três formas: SQL embutido em
serviços, chamadas diretas de ORM em controllers e acesso a dados copiado em jobs. Cada ponto de
contato acopla o domínio ao formato físico do armazenamento, e uma mudança de schema ou de banco
vira varredura em dezenas de arquivos. Testar um caso de uso nesse cenário exige banco real ou
containers, o que deixa a suíte lenta e reduz o número de execuções no dia. Há ainda o problema de
regra implícita: uma consulta com filtro escondido, como "somente pedidos não cancelados", repetida
em cinco lugares, fica em cinco versões divergentes depois de um ano. O repositório concentra esse
acesso em um contrato nomeado pela linguagem do domínio, de implementação única e substituível.
Contrato de coleção de agregados
O contrato do repositório imita uma coleção do domínio, com três grupos de operações. Métodos de
inclusão e alteração, como adicionar, tratam o agregado como unidade indivisível: salvar um pedido
salva raiz, itens e objetos de valor embutidos. Métodos de busca, como buscarPorId e buscarAtivosDoCliente,
devolvem agregados inteiros, prontos para uso, nunca entidades internas soltas. Métodos de
remoção, como remover, também atuam na unidade inteira. A interface fala a linguagem do negócio:
buscarAssinaturasInadimplentes diz exatamente o que devolve, e a query SQL correspondente é
detalhe da implementação. O limite saudável é não expor conceitos de infraestrutura no contrato,
como paginação de cursor do banco, filtros genéricos por nome de coluna ou retornos de linha crua.
Um repositório por agregado
A regra de granularidade é um repositório por raiz de agregado, e não por tabela. Se o agregado
Pedido cobre pedido e itens, existe um RepositorioDePedidos, e nada de RepositorioDeItensDoPedido,
porque itens não têm vida fora da raiz. Essa correspondência mantém a fronteira transacional do
agregado refletida na camada de acesso a dados, e evita o caminho clássico de corrupção de
invariantes, que é salvar uma parte do agregado por meio de outro repositório. Compartilhar um
repositório entre duas raízes de agregados diferentes também é sinal de fronteira mal definida: o
contrato fica cheio de métodos que só uma das raízes usa, e a implementação acaba carregando
transações que cruzam agregados. Quando o agregado é grande, as consultas de leitura intensa
merecem tratamento à parte, o que será discutido adiante.
Interface no domínio, implementação na infraestrutura
A interface do repositório vive no pacote de domínio e a implementação vive na infraestrutura, com
a dependência apontando de fora para dentro. Esse é o princípio da inversão de dependência aplicado
à persistência: o domínio declara o que precisa, sem saber como é entregue. O exemplo a seguir
mostra o desenho típico em Java.
// No módulo de domínio: contrato na linguagem do negócio, sem tipos de infraestrutura
public interface RepositorioDePedidos {
void adicionar(Pedido pedido);
Optional<Pedido> buscarPorId(PedidoId id);
List<Pedido> buscarAbertosDoCliente(ClienteId clienteId); // nome do negócio, não da query
}
// Na infraestrutura: a única classe que conhece o ORM e o schema
public final class RepositorioDePedidosJpa implements RepositorioDePedidos {
private final EntityManager em; // detalhe de infraestrutura
@Override
public Optional<Pedido> buscarPorId(PedidoId id) {
return Optional.ofNullable(em.find(Pedido.class, id)); // carrega raiz e itens do agregado
}
@Override
public List<Pedido> buscarAbertosDoCliente(ClienteId clienteId) {
String jpql = """
select p from Pedido p
where p.clienteId = :cliente and p.status = 'ABERTO'
""";
return em.createQuery(jpql, Pedido.class)
.setParameter("cliente", clienteId)
.getResultList();
}
@Override
public void adicionar(Pedido pedido) {
em.persist(pedido); // persiste o agregado inteiro
}
}
No caso de uso, a injeção é feita pela interface, e o serviço de aplicação não sabe que existe
JPA no mundo. Trocar a implementação por uma versão com cache Redis à frente do banco, ou por um
banco diferente, não muda uma linha do domínio.
Repositório versus DAO genérico
A diferença entre repositório e DAO não é o nome, e sim a granularidade e a semântica do contrato.
Um DAO genérico expõe operações de tabela: save(Entity), findById(Long), deleteAll() e
critérios de consulta arbitrários, e serve qualquer objeto persistente. O repositório expõe
conceitos: ele só existe para um agregado, fala o vocabulário do contexto e pode internamente
montar o agregado a partir de várias tabelas sem que o chamador saiba. O DAO não impede acesso
direto às partes internas do agregado, e é justamente esse acesso que corrompe invariantes.
| Critério | Repositório | DAO genérico |
|---|---|---|
| Unidade de trabalho | Agregado completo | Linha ou entidade de tabela |
| Vocabulário | Domínio, com nomes de negócio | Técnico, com nomes genéricos |
| Clientes típicos | Casos de uso e serviços de aplicação | Qualquer camada que precise de CRUD |
| Exposição interna | Nunca devolve entidades internas | Devolve qualquer objeto persistente |
| Reutilização | Uma interface por agregado | Uma interface genérica para tudo |
| Efeito no modelo | Protege invariantes e fronteiras | Neutraliza o encapsulamento do agregado |
Testes e o papel do repositório na suíte
A separação entre interface e implementação cria o ponto de teste mais produtivo da arquitetura
hexagonal. Casos de uso são testados com um repositório em memória, uma implementação da interface
que guarda objetos em um HashMap, e rodam em milissegundos sem container. Esses testes exercitam
regras de domínio reais com dados de cenário, e a suíte grande fica rápida o suficiente para rodar
a cada push. A implementação real, com banco, recebe uma suíte menor de testes de integração, que
verifica mapeamento, queries e migrações. Um cuidado importante: o repositório em memória deve
implementar a mesma semântica de ordenação e unicidade do contrato, senão o teste passa e a produção
falha em casos de duplicidade. Eventuais consultas críticas de performance são validadas na suíte de
integração, não na em memória.
Erros comuns e boas práticas
- Criar um repositório por tabela, espelhando o banco. A granularidade correta é o agregado, e
repositório por tabela reinventa o DAO e quebra a fronteira transacional. - Expôr IQueryable, Specification genérica ou filtros por nome de coluna na interface. Esses
escapes devolvem ao chamador o poder de montar qualquer consulta e recriam o acoplamento. - Devolver entidades internas do agregado. O contrato devolve a raiz, e partes internas nunca
atravessam a fronteira. - Colocar regra de negócio dentro da implementação do repositório. Repositório lê e grava estado;
decisão sobre estado é trabalho do domínio. - Fazer o repositório disparar eventos ou transações externas. Publicação de evento é papel da
aplicação após a persistência, e misturar os dois complica rollback e repetição. - Deixar consultas de leitura pesada passando pelo agregado completo. Para telas e relatórios,
consultas dedicadas fora do modelo são aceitáveis e até recomendadas.