DDD: Repositories - Abstraindo o acesso aos dados (Parte 7)
Este é o sétimo artigo da série sobre Domain-Driven Design. Nos artigos anteriores, exploramos conceitos fundamentais, Value Objects, Entities, Agregados e Services. Agora chegou o momento de entender um padrão essencial para manter a pureza do modelo de domínio: Repository Pattern.
O Repository é um dos padrões mais importantes - e ao mesmo tempo mais mal compreendidos - do DDD. Ele não é apenas uma abstração sobre um banco de dados; é a ponte entre o modelo de domínio e a camada de persistência.
O que é o Padrão Repository?
O Repository Pattern fornece uma abstração sobre a camada de acesso a dados, encapsulando a lógica necessária para acessar fontes de dados. Martin Fowler o define assim:
"Um Repository encapsula o conjunto de objetos persistidos em um armazenamento de dados e as operações realizadas sobre eles, fornecendo uma visão mais orientada a objeto da camada de persistência."
Eric Evans, no contexto do DDD, vai além:
"Um Repository representa todos os objetos de um certo tipo como uma coleção conceitual (geralmente emulada). Ele age como uma coleção em memória de todos os objetos desse tipo."
A Diferença entre Repository e DAO
É crucial entender que Repository Pattern não é a mesma coisa que Data Access Object (DAO). Embora ambos lidem com persistência, suas responsabilidades são bem diferentes:
DAO (Data Access Object):
- Foca em operações CRUD básicas
- Geralmente mapeia 1:1 com tabelas do banco
- Expõe detalhes da persistência
- Trabalha com registros/linhas
Repository:
- Foca em coleções de objetos de domínio
- Trabalha com Aggregate Roots completos
- Esconde detalhes da persistência
- Trabalha com objetos de domínio ricos
Por que Usar Repository no DDD?
O Repository no DDD tem objetivos específicos que vão além da simples abstração de dados:
1. Preserve a Ignorância da Persistência
O modelo de domínio deve ser Persistence Ignorant - ele não deve saber como é persistido. O Repository mantém essa separação:
// ❌ Modelo de domínio ciente da persistência
class Usuario
{
private $pdo;
public function salvar(): void
{
$sql = "UPDATE usuarios SET nome = ?, email = ? WHERE id = ?";
$this->pdo->prepare($sql)->execute([$this->nome, $this->email, $this->id]);
}
}
// ✅ Modelo de domínio puro
class Usuario
{
public function alterarNome(string $novoNome): void
{
$this->validarNome($novoNome);
$this->nome = $novoNome;
// Domínio puro - sem SQL ou persistência
}
}
2. Trabalhe com Aggregate Roots
Repositories sempre operam no nível de Aggregate Roots. Eles carregam e salvam agregados completos, respeitando suas fronteiras de consistência:
interface PedidoRepository
{
public function buscarPorId(PedidoId $id): ?Pedido;
public function salvar(Pedido $pedido): void;
public function remover(PedidoId $id): void;
public function buscarPedidosPendentes(): array;
}
3. Forneça uma Interface Rica de Consulta
Repositories devem expressar consultas na linguagem do domínio, não em termos técnicos:
interface ClienteRepository
{
// ❌ Interface técnica
public function findByStatus(string $status): array;
public function findByDateRange(DateTime $start, DateTime $end): array;
// ✅ Interface de domínio
public function buscarClientesAtivos(): array;
public function buscarClientesComPedidosRecentes(): array;
public function buscarClientesElegiveisParaDesconto(): array;
}
Implementando Repository Pattern
Definindo a Interface
A interface do Repository deve ser definida na camada de domínio e expressar conceitos do negócio:
interface ContaBancariaRepository
{
public function proximoId(): ContaId;
public function buscarPorId(ContaId $id): ?ContaBancaria;
public function buscarPorCpf(Cpf $cpf): ?ContaBancaria;
public function buscarContasAtivas(): array;
public function buscarContasComSaldoNegativo(): array;
public function salvar(ContaBancaria $conta): void;
public function remover(ContaId $id): void;
}
Implementação com Abstração de Dados
A implementação concreta fica na camada de infraestrutura:
class PDOContaBancariaRepository implements ContaBancariaRepository
{
public function __construct(
private readonly PDO $pdo,
private readonly ContaBancariaMapper $mapper
) {}
public function proximoId(): ContaId
{
return ContaId::gerar();
}
public function buscarPorId(ContaId $id): ?ContaBancaria
{
$sql = "
SELECT c.*, t.*
FROM contas_bancarias c
LEFT JOIN transacoes t ON c.id = t.conta_id
WHERE c.id = ? AND c.ativa = 1
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$id->valor]);
$dados = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($dados)) {
return null;
}
return $this->mapper->arrayParaContaBancaria($dados);
}
public function buscarPorCpf(Cpf $cpf): ?ContaBancaria
{
$sql = "
SELECT c.*, t.*
FROM contas_bancarias c
LEFT JOIN transacoes t ON c.id = t.conta_id
WHERE c.cpf_titular = ? AND c.ativa = 1
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$cpf->valor]);
$dados = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($dados)) {
return null;
}
return $this->mapper->arrayParaContaBancaria($dados);
}
public function buscarContasAtivas(): array
{
$sql = "
SELECT c.*, t.*
FROM contas_bancarias c
LEFT JOIN transacoes t ON c.id = t.conta_id
WHERE c.ativa = 1
ORDER BY c.created_at DESC
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return $this->mapper->criarContasDeResultados($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function buscarContasComSaldoNegativo(): array
{
$sql = "
SELECT c.*, t.*
FROM contas_bancarias c
LEFT JOIN transacoes t ON c.id = t.conta_id
WHERE c.saldo_atual < 0 AND c.ativa = 1
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return $this->mapper->criarContasDeResultados($stmt->fetchAll(PDO::FETCH_ASSOC));
}
public function salvar(ContaBancaria $conta): void
{
$this->pdo->beginTransaction();
try {
$this->salvarConta($conta);
$this->salvarTransacoes($conta);
$this->pdo->commit();
} catch (Exception $e) {
$this->pdo->rollback();
throw new RepositoryException("Erro ao salvar conta bancária", 0, $e);
}
}
public function remover(ContaId $id): void
{
// Soft delete - marcamos como inativa
$sql = "UPDATE contas_bancarias SET ativa = 0 WHERE id = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$id->valor]);
}
private function salvarConta(ContaBancaria $conta): void
{
if ($this->contaExiste($conta->getId())) {
$this->atualizarConta($conta);
} else {
$this->inserirConta($conta);
}
}
private function salvarTransacoes(ContaBancaria $conta): void
{
// Aqui salvamos apenas transações novas
// Em uma implementação real, você poderia usar eventos de domínio
// ou outro mecanismo para identificar mudanças
$transacoesNovas = $conta->getTransacoesNaoSalvas();
foreach ($transacoesNovas as $transacao) {
$this->inserirTransacao($transacao);
}
}
// ... outros métodos privados para operações SQL específicas
}
Usando Specification Pattern
Para consultas complexas, podemos combinar Repository com o Specification Pattern:
interface Specification
{
public function satisfeita($objeto): bool;
public function toSqlWhere(): string;
public function getParametros(): array;
}
class ClienteComMaisDeXPedidosSpecification implements Specification
{
public function __construct(private readonly int $minimoPedidos) {}
public function satisfeita($cliente): bool
{
return $cliente->getQuantidadePedidos() >= $this->minimoPedidos;
}
public function toSqlWhere(): string
{
return "
EXISTS (
SELECT COUNT(*)
FROM pedidos p
WHERE p.cliente_id = c.id
HAVING COUNT(*) >= ?
)
";
}
public function getParametros(): array
{
return [$this->minimoPedidos];
}
}
// No Repository
public function buscarClientesPorSpecification(Specification $spec): array
{
$sql = "SELECT * FROM clientes c WHERE " . $spec->toSqlWhere();
$stmt = $this->pdo->prepare($sql);
$stmt->execute($spec->getParametros());
return $this->mapper->criarClientesDeResultados($stmt->fetchAll());
}
Repository vs ORM
Uma questão comum é a relação entre Repository Pattern e ORMs como Eloquent, Hibernate ou Doctrine.
Repository com ORM
ORMs podem ser usados dentro da implementação do Repository, mas não devem vazar para o domínio:
class DoctrineUsuarioRepository implements UsuarioRepository
{
public function __construct(private readonly EntityManager $em) {}
public function buscarPorId(UsuarioId $id): ?Usuario
{
return $this->em->find(Usuario::class, $id->valor);
}
public function buscarUsuariosAtivos(): array
{
return $this->em->createQueryBuilder()
->select('u')
->from(Usuario::class, 'u')
->where('u.ativo = :ativo')
->setParameter('ativo', true)
->getQuery()
->getResult();
}
public function salvar(Usuario $usuario): void
{
$this->em->persist($usuario);
// Note: não chamamos flush aqui - isso é responsabilidade da camada de aplicação
}
}
Repository Sem ORM
Também é perfeitamente válido implementar Repository sem ORM:
class PDOUsuarioRepository implements UsuarioRepository
{
public function __construct(
private readonly PDO $pdo,
private readonly UsuarioMapper $mapper
) {}
public function buscarPorId(UsuarioId $id): ?Usuario
{
$sql = "SELECT * FROM usuarios WHERE id = ? AND ativo = 1";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$id->valor]);
$dados = $stmt->fetch(PDO::FETCH_ASSOC);
return $dados ? $this->mapper->arrayParaUsuario($dados) : null;
}
public function salvar(Usuario $usuario): void
{
$dados = $this->mapper->usuarioParaArray($usuario);
if ($this->usuarioExiste($usuario->getId())) {
$this->atualizar($dados);
} else {
$this->inserir($dados);
}
}
}
Repository e Aggregate Roots
Um dos princípios fundamentais é que cada Aggregate Root deve ter seu próprio Repository:
// ✅ Correto - Repository por Aggregate Root
interface PedidoRepository
{
public function buscarPorId(PedidoId $id): ?Pedido;
public function salvar(Pedido $pedido): void;
}
interface ClienteRepository
{
public function buscarPorId(ClienteId $id): ?Cliente;
public function salvar(Cliente $cliente): void;
}
// ❌ Incorreto - Repository para entidades filhas
interface ItemPedidoRepository // ItemPedido é parte do agregado Pedido
{
public function buscarPorId(ItemId $id): ?ItemPedido;
}
Carregando Agregados Completos
O Repository deve sempre carregar o agregado completo, incluindo suas entidades filhas:
class PDOPedidoRepository implements PedidoRepository
{
public function buscarPorId(PedidoId $id): ?Pedido
{
// Carrega pedido + itens + pagamentos em uma única operação
$sql = "
SELECT
p.*,
i.id as item_id, i.produto_id, i.quantidade, i.preco_unitario,
pg.id as pagamento_id, pg.tipo, pg.valor, pg.status
FROM pedidos p
LEFT JOIN itens_pedido i ON p.id = i.pedido_id
LEFT JOIN pagamentos pg ON p.id = pg.pedido_id
WHERE p.id = ?
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$id->valor]);
$dados = $stmt->fetchAll(PDO::FETCH_ASSOC);
return empty($dados) ? null : $this->mapper->arrayParaPedido($dados);
}
}
Testabilidade com Repository
Repository Pattern facilita muito os testes, permitindo implementações in-memory:
class InMemoryUsuarioRepository implements UsuarioRepository
{
private array $usuarios = [];
public function buscarPorId(UsuarioId $id): ?Usuario
{
return $this->usuarios[$id->valor] ?? null;
}
public function buscarPorEmail(Email $email): ?Usuario
{
foreach ($this->usuarios as $usuario) {
if ($usuario->getEmail()->valor === $email->valor) {
return $usuario;
}
}
return null;
}
public function salvar(Usuario $usuario): void
{
$this->usuarios[$usuario->getId()->valor] = $usuario;
}
public function remover(UsuarioId $id): void
{
unset($this->usuarios[$id->valor]);
}
}
// No teste
class CriarUsuarioServiceTest extends TestCase
{
public function testCriarUsuarioComEmailUnico(): void
{
// Arrange
$repository = new InMemoryUsuarioRepository();
$service = new CriarUsuarioService($repository);
// Act
$usuario = $service->criar('João Silva', 'joao@email.com');
// Assert
$this->assertNotNull($repository->buscarPorId($usuario->getId()));
$this->assertEquals('João Silva', $usuario->getNome()->valor);
}
}
Padrões e Anti-Padrões
✅ Boas Práticas
- Uma interface por Aggregate Root
interface PedidoRepository
{
public function buscarPorId(PedidoId $id): ?Pedido;
public function salvar(Pedido $pedido): void;
}
- Métodos expressivos da linguagem de domínio
public function buscarPedidosPendentesDeAprovacao(): array;
public function buscarClientesComRiscoDeCredito(): array;
- Repository retorna objetos de domínio completos
public function buscarPorId(PedidoId $id): ?Pedido
{
// Carrega pedido completo com itens, pagamentos, etc.
}
❌ Anti-Padrões
- Repository genérico
// ❌ Muito genérico
interface Repository<T>
{
public function find(int $id): ?T;
public function save(T $entity): void;
}
- Vazamento de detalhes de persistência
// ❌ Expõe SQL/ORM para o domínio
public function buscarPorQuery(string $sql): array;
public function buscarComJoin(array $joins): array;
- Repository para entidades não-root
// ❌ ItemPedido faz parte do agregado Pedido
interface ItemPedidoRepository
{
public function salvar(ItemPedido $item): void;
}
Repository em Arquiteturas Modernas
CQRS e Repository
Em arquiteturas CQRS, Repository geralmente é usado apenas no lado Command:
// Command Side - usa Repository
class ProcessarPedidoCommandHandler
{
public function __construct(
private readonly PedidoRepository $pedidoRepository,
private readonly ClienteRepository $clienteRepository
) {}
public function handle(ProcessarPedidoCommand $command): void
{
$cliente = $this->clienteRepository->buscarPorId($command->clienteId);
$pedido = Pedido::criar($cliente, $command->itens);
$this->pedidoRepository->salvar($pedido);
}
}
// Query Side - acesso direto aos dados otimizado
class ListarPedidosQueryHandler
{
public function __construct(private readonly PDO $pdo) {}
public function handle(ListarPedidosQuery $query): array
{
// Query otimizada específica para leitura
$sql = "
SELECT p.id, p.numero, c.nome as cliente_nome, p.valor_total, p.status
FROM pedidos p
JOIN clientes c ON p.cliente_id = c.id
WHERE p.cliente_id = ?
ORDER BY p.created_at DESC
";
return $this->pdo->prepare($sql)->execute([$query->clienteId])->fetchAll();
}
}
Event Sourcing e Repository
Com Event Sourcing, Repository trabalha com streams de eventos:
interface EventSourcedRepository
{
public function carregarEventos(AggregateId $id): EventStream;
public function salvarEventos(AggregateId $id, EventStream $eventos): void;
}
class EventSourcedPedidoRepository implements EventSourcedRepository
{
public function buscarPorId(PedidoId $id): ?Pedido
{
$eventos = $this->carregarEventos($id);
if ($eventos->isEmpty()) {
return null;
}
return Pedido::reconstituirDeEventos($eventos);
}
public function salvar(Pedido $pedido): void
{
$eventosNovos = $pedido->getEventosNaoComitados();
$this->salvarEventos($pedido->getId(), $eventosNovos);
$pedido->marcarEventosComoComitados();
}
}
Conclusão
O Repository Pattern no DDD é muito mais que uma abstração sobre banco de dados. É o guardião da integridade do modelo de domínio, mantendo-o puro e focado na lógica de negócio.
Pontos-chave para lembrar:
- Repository trabalha com Aggregate Roots, não com tabelas
- Interface definida no domínio, implementação na infraestrutura
- Expressa conceitos de negócio nas operações disponíveis
- Mantém o domínio persistence-ignorant
- Um Repository por Aggregate Root
- Carrega e salva agregados completos
- Facilita testes com implementações in-memory
Quando bem implementado, Repository permite que seu modelo de domínio seja rico, expressivo e completamente independente de detalhes de persistência. Ele é a ponte que permite que seu código de negócio permaneça limpo enquanto ainda oferece persistência robusta e flexível.
No próximo artigo da série, exploraremos Domain Events, entendendo como comunicar mudanças entre diferentes partes do sistema de forma desacoplada.
Anterior
DDD: Domain Services vs Application Services - Organizando a lógica de negócio (Parte 6)
Próximo
DDD: Domain Events - Comunicação entre contextos (Parte 8)
Referências
- Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans
- Patterns of Enterprise Application Architecture - Martin Fowler
- Repository Pattern - Martin Fowler
- DDD — Repository Pattern: quais os benefícios? - Elisandro Mello
- The Repository Pattern Done Right - Matías Navarro-Carter
- Understanding the Repositories Patterns in Domain-Driven Design - Kranio
- Repository Pattern - DevIQ
- Padrão Repository: A Abstração que Transformou o Acesso a Dados - Gustavo Cremonez
- Implementing Domain-Driven Design - Vaughn Vernon