Repository Pattern no Laravel: quando usar, quando dispensar a interface e quando evitar

Repository Pattern no Laravel: quando usar, quando dispensar a interface e quando evitar

Uma resposta comum sobre o Repository Pattern no Laravel é que ele serve apenas para abstrair o Eloquent. A definição não está errada, mas deixa de fora o motivo de o padrão existir, e é justamente esse motivo que decide quando ele vale a pena num projeto Laravel e quando vira só uma pasta a mais.

Este artigo junta a definição original do padrão, a diferença entre repository, DAO e as camadas da Arquitetura Limpa, e exemplos práticos de como usar repository no Laravel com e sem interface.

O que é o Repository Pattern

O Repository foi descrito em 2003 no livro Patterns of Enterprise Application Architecture, de Martin Fowler, uma das principais referências sobre padrões para sistemas corporativos. No site do autor, a página dedicada ao padrão resume a ideia: o repository fica entre o domínio (as regras do negócio) e a camada que mapeia objetos para o banco, e se comporta como uma coleção de objetos em memória.

Esse detalhe da coleção é o ponto central. Quem usa o repository não pensa em tabela, join ou SQL. Pensa em "me dê os fornecedores ativos" ou "guarde este pedido". O repository traduz esse pedido para o mecanismo de persistência que estiver por baixo.

A mesma página aponta dois ganhos: concentrar a montagem de consultas num lugar só, o que evita lógica de consulta duplicada, e manter a dependência num sentido único, com o domínio sem saber nada do código de mapeamento. Eric Evans retomou o padrão no livro Domain-Driven Design, também de 2003, e é dali que vem boa parte do uso atual.

O tutorial de Oluyemi Olususi publicado em português no blog da Twilio parte da mesma definição e destaca como principal benefício a inversão de dependência: o código passa a depender de uma abstração, não de uma classe concreta. Guarde essa ideia, porque ela volta mais adiante.

O Eloquent já é Active Record

Para entender por que o repository soa estranho no Laravel, vale lembrar como o Eloquent funciona. Ele segue o padrão Active Record: o próprio model sabe ir ao banco, consultar e salvar. O Doctrine, ORM padrão do Symfony, segue o Data Mapper, em que a entidade é uma classe PHP comum e outra peça cuida da persistência. O curso de Doctrine da Alura explica essa diferença de forma bem direta.

No Doctrine, cada entidade já tem um repository correspondente. No Laravel, não existe nada parecido de fábrica, e o model faz as duas coisas.

Isso gera uma consequência que pouca gente comenta. Mesmo com um repository na frente, qualquer model Eloquent continua capaz de chamar save() ou delete() de qualquer lugar do código. O repository no Laravel é uma convenção do time, não uma barreira técnica. Ele só funciona se todo mundo concordar em passar por ele.

Repository só abstrai o Eloquent?

Em parte, sim. Esconder o mecanismo de persistência é a função do repository. Num projeto Laravel, esse mecanismo quase sempre é o Eloquent, então dizer que ele abstrai o Eloquent descreve bem o que acontece na prática.

O que essa descrição deixa de fora são duas coisas. A primeira é a linguagem: o repository expõe métodos com nome de negócio (activeWithExpiredDocuments()), não métodos genéricos que repetem o Eloquent com outro nome (where(), all()). A segunda é a direção da dependência: quem usa o repository depende de um contrato, e a implementação é que se adapta a ele.

Um repository que só repassa chamadas para o model, com all(), find(), create() e update(), abstrai o Eloquent no sentido mais pobre da palavra. Ele troca uma API conhecida por outra que faz a mesma coisa.

Repository e DAO

Boa parte da confusão vem de outro padrão, o DAO (Data Access Object). O artigo de Sebastião Almeida na DIO resume bem a diferença: o DAO é um padrão de integração, mais próximo do banco, que persiste entidades uma a uma. O repository é um padrão de domínio, que lida com agregados (grupos de entidades que andam juntas, como um pedido e seus itens).

O mesmo texto lembra, citando o Baeldung, que num domínio anêmico (classes que só carregam dados, sem comportamento) o repository acaba sendo apenas um DAO. É exatamente o caso da maioria dos CRUDs em Laravel. Se o "repository" do seu projeto só faz insert, update, delete e select por id, ele é um DAO com outro nome. Não é um problema, mas é bom saber o que se tem nas mãos.

Onde o repository fica na Arquitetura Limpa

Na Arquitetura Limpa, Robert C. Martin organiza o sistema em círculos: Entidades (regras gerais do negócio), Casos de Uso (regras específicas da aplicação), Adaptadores de Interface e, por fora, Frameworks e Drivers. A regra principal é que o código só pode depender de círculos mais internos.

Segundo o texto original, é na camada de Adaptadores de Interface que os dados são convertidos para o formato do banco, e todo o SQL deve ficar restrito a ela. O banco em si, assim como o framework web, fica no círculo mais externo, tratado como detalhe.

O repository atravessa essa fronteira de um jeito específico. A interface (o contrato) mora num círculo interno, junto do domínio ou dos casos de uso. A implementação concreta, que usa Eloquent ou SQL, mora em Adaptadores de Interface. Nas anotações de Marcel Santos sobre a palestra de Matthias Noback a respeito de arquitetura hexagonal, a ideia aparece com clareza: a camada interna não pode depender da externa, então o repository vira interface no domínio e a infraestrutura implementa essa interface. Isso é inversão de dependência.

Então, quando alguém diz que o repository "abstrai o Eloquent", está falando da implementação, que de fato vive na camada de adaptadores. Quando o assunto é Arquitetura Limpa, o mais importante é o contrato, que vive no centro.

Repository não é service nem caso de uso

A outra camada que costuma se misturar com o repository é a de Casos de Uso, que no Laravel aparece como Service ou Action. Ali ficam as regras de negócio: validar se um CNPJ já está cadastrado, disparar um evento, decidir o que acontece quando um documento vence.

O repository busca e guarda dados. Se ele começa a validar entrada, receber Request ou devolver resposta HTTP, virou outra coisa com o nome de repository. Um exemplo de cada papel:

<?php

namespace App\Actions;

use App\Events\SupplierRegistered;
use App\Exceptions\DuplicatedCnpjException;
use App\Models\Supplier;
use App\Repositories\SupplierRepository;

// Caso de uso: regra de negócio e orquestração
class RegisterSupplier
{
    public function __construct(
        private SupplierRepository $suppliers,
    ) {}

    public function handle(array $data): Supplier
    {
        if ($this->suppliers->existsByCnpj($data['cnpj'])) {
            throw new DuplicatedCnpjException($data['cnpj']);
        }

        $supplier = $this->suppliers->store($data);

        SupplierRegistered::dispatch($supplier);

        return $supplier;
    }
}

A Action decide. O repository só responde se o CNPJ existe e grava o registro.

Repository sem interface

A maioria dos tutoriais começa criando SupplierRepositoryInterface, depois a classe concreta, depois um Service Provider para ligar uma à outra. Em boa parte dos projetos, a interface é dispensável.

A documentação do Laravel explica o motivo. O container resolve sozinho qualquer classe que dependa apenas de outras classes concretas, sem configuração nenhuma. Registrar um binding manual só é necessário quando o construtor pede uma interface. Ou seja, uma classe concreta já pode ser injetada no controller ou na Action.

<?php

namespace App\Repositories;

use App\Models\Supplier;
use Illuminate\Database\Eloquent\Collection;

class SupplierRepository
{
    public function existsByCnpj(string $cnpj): bool
    {
        return Supplier::query()->where('cnpj', $cnpj)->exists();
    }

    public function store(array $data): Supplier
    {
        return Supplier::query()->create($data);
    }

    public function activeWithExpiredDocuments(): Collection
    {
        return Supplier::query()
            ->where('active', true)
            ->whereHas('documents', fn ($query) => $query->whereDate('expires_at', '<', now()))
            ->with('documents')
            ->orderBy('name')
            ->get();
    }
}

Repare que o método activeWithExpiredDocuments() é o tipo de consulta que justifica o repository: tem filtro, relacionamento, eager loading e ordenação, e provavelmente é usada num relatório, num job e numa notificação. Mudar a regra de "documento vencido" passa a ser uma alteração num lugar só.

E o teste? O argumento mais comum a favor da interface é a facilidade de criar um dublê. Só que o Laravel já permite mockar uma classe concreta direto pelo container:

<?php

use App\Actions\RegisterSupplier;
use App\Exceptions\DuplicatedCnpjException;
use App\Repositories\SupplierRepository;
use Mockery\MockInterface;

test('does not register a supplier with a duplicated cnpj', function () {
    $this->mock(SupplierRepository::class, function (MockInterface $mock) {
        $mock->shouldReceive('existsByCnpj')->once()->andReturn(true);
        $mock->shouldNotReceive('store');
    });

    app(RegisterSupplier::class)->handle(['cnpj' => '12345678000199']);
})->throws(DuplicatedCnpjException::class);

Um cuidado: o Mockery não consegue estender classe marcada como final. Se o repository vai ser mockado, deixe a classe sem final ou use uma interface.

Quando a interface faz sentido

A interface passa a pagar o custo quando existe, ou vai existir de verdade, mais de uma implementação. Alguns casos comuns:

  • Os dados vêm de lugares diferentes conforme o contexto, como banco local num ambiente e API de um sistema legado em outro.
  • O projeto separa domínio e infraestrutura em módulos, no estilo da Arquitetura Limpa ou hexagonal, e o domínio não pode conhecer o Eloquent.
  • Os testes usam uma implementação em memória, mais rápida e previsível do que um mock configurado chamada a chamada.
  • O código é um pacote que outras pessoas vão usar e precisam trocar a implementação.

Nas versões recentes do Laravel, nem o Service Provider é obrigatório. O atributo #[Bind] fica na própria interface e pode indicar uma implementação diferente por ambiente:

<?php

namespace App\Contracts;

use App\Models\Supplier;
use App\Repositories\EloquentSupplierRepository;
use App\Repositories\InMemorySupplierRepository;
use Illuminate\Container\Attributes\Bind;

#[Bind(EloquentSupplierRepository::class)]
#[Bind(InMemorySupplierRepository::class, environments: ['testing'])]
interface SupplierRepository
{
    public function existsByCnpj(string $cnpj): bool;

    public function store(array $data): Supplier;
}

Mesmo com interface, vale uma honestidade: se os métodos devolvem models e collections do Eloquent, o contrato continua amarrado ao Eloquent. A troca de ORM, nesse caso, exigiria reescrever também quem consome o repository. Para isolar de verdade, o repository precisaria devolver objetos próprios do domínio (entidades simples ou DTOs), o que aumenta bastante o código. Na maioria dos projetos Laravel, esse acoplamento parcial é uma troca aceitável, desde que seja uma decisão consciente.

Alternativas do próprio Laravel

Se o objetivo é só organizar consultas repetidas, o Eloquent já tem ferramentas para isso, sem camada extra.

Os scopes locais resolvem filtros reaproveitáveis dentro do model. Quando o model começa a acumular scopes demais, dá para movê-los para um query builder customizado. Desde o Laravel 12.19, o atributo #[UseEloquentBuilder] registra esse builder sem precisar sobrescrever newEloquentBuilder():

<?php

namespace App\Builders;

use Illuminate\Database\Eloquent\Builder;

class SupplierBuilder extends Builder
{
    public function active(): self
    {
        return $this->where('active', true);
    }

    public function withExpiredDocuments(): self
    {
        return $this->whereHas(
            'documents',
            fn ($query) => $query->whereDate('expires_at', '<', now())
        );
    }
}
<?php

namespace App\Models;

use App\Builders\SupplierBuilder;
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
use Illuminate\Database\Eloquent\Model;

#[UseEloquentBuilder(SupplierBuilder::class)]
class Supplier extends Model
{
    // ...
}

// Uso
$suppliers = Supplier::query()->active()->withExpiredDocuments()->get();

A consulta continua encadeável, com paginação, eager loading e tudo o que o Eloquent oferece. Para a regra de negócio, o lugar é a Action ou o Service, como no exemplo do RegisterSupplier.

Quando evitar

O repository tende a atrapalhar mais do que ajudar quando:

  • O sistema é um CRUD, e os métodos do repository repetem all(), find(), create() e update() do Eloquent.
  • O argumento é "um dia vamos trocar de banco". Trocar PostgreSQL por MySQL não pede repository, o Eloquent já resolve. Trocar o Eloquent por outro ORM é raro e, como visto acima, a interface sozinha não garante a troca.
  • O repository vira um repositório genérico com where() e orderBy() públicos. Nesse ponto ele só reimplementa o query builder, e pior.
  • A camada existe apenas para seguir um template de projeto, sem que ninguém saiba qual problema ela resolve.

E faz sentido quando há consultas complexas usadas em vários lugares, fontes de dados diferentes, um domínio rico com agregados ou uma arquitetura em camadas que o time realmente segue.

Resumindo

O repository abstrai a persistência, e no Laravel isso quase sempre significa abstrair o Eloquent. Mas o valor do padrão está em dois pontos que a descrição curta esconde: expor operações com a linguagem do negócio e permitir que o código de regra dependa de um contrato, não da implementação.

No Laravel, comece pelo mais simples. Scopes e query builders resolvem a organização de consultas. Um repository concreto, sem interface, resolve consultas complexas reaproveitadas e continua fácil de testar. A interface entra quando há mais de uma implementação ou quando a arquitetura separa domínio e infraestrutura de fato. E a regra de negócio fica sempre fora do repository, na Action ou no Service.

Referências

  • ALMEIDA, Sebastião. DDD: a diferença entre Dao e Repository. DIO, 2022. Disponível em: https://www.dio.me/articles/ddd-a-diferenca-entre-dao-e-repository. Acesso em: 7 out. 2026.
  • ALURA. Doctrine: conhecendo um ORM PHP. Alura, [s.d.]. Disponível em: https://www.alura.com.br/conteudo/doctrine-conhecendo-orm-php. Acesso em: 7 out. 2026.
  • BAELDUNG. DAO vs Repository Patterns. Baeldung, [s.d.]. Disponível em: https://www.baeldung.com/java-dao-vs-repository. Acesso em: 7 out. 2026.
  • HIEATT, Edward; MEE, Rob. Repository. In: FOWLER, Martin. Catalog of Patterns of Enterprise Application Architecture. martinfowler.com, 2003. Disponível em: https://martinfowler.com/eaaCatalog/repository.html. Acesso em: 7 out. 2026.
  • LARAVEL. Service Container. Laravel 13.x Documentation, 2026. Disponível em: https://laravel.com/docs/13.x/container. Acesso em: 7 out. 2026.
  • LARAVEL NEWS. Defining a Dedicated Query Builder in Laravel 12 With PHP Attributes. Laravel News, 2025. Disponível em: https://laravel-news.com/defining-a-dedicated-query-builder-in-laravel-12-with-php-attributes. Acesso em: 7 out. 2026.
  • MARTIN, Robert C. The Clean Architecture. The Clean Code Blog, 2012. Disponível em: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html. Acesso em: 7 out. 2026.
  • OLUSUSI, Oluyemi. Como usar o padrão de repositório em um aplicativo Laravel. Twilio Blog, 2021. Disponível em: https://www.twilio.com/pt-br/blog/repository-pattern-in-laravel-application. Acesso em: 7 out. 2026.
  • SANTOS, Marcel. Hexagonal Architecture: Message-Oriented Software Design, por Matthias Noback (anotações). GitHub Gist, 2018. Disponível em: https://gist.github.com/marcelgsantos/f630cbc41c5109ee8ee03d7960c6fa65. Acesso em: 7 out. 2026.

Comentários (0)

Nenhum comentário ainda. Seja o primeiro.

Continue lendo

Como rodar Laravel no WSL com Valet Linux (de verdade)

Como rodar Laravel no WSL com Valet Linux (de verdade)

Sim, dá pra desenvolver Laravel no Windows sem sofrer. Mas você precisa fazer do jeito certo: usando WSL + Valet Linux. Esqueça XAMPP, Laragon, Docker lento e até o WSL 1. O caminho mais fluido para trabalhar com…

Laravel, NGINX, php, Tutorial