Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

04 - Repositório (Repository)

#ddd#persistencia#repository#testabilidade
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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.