The repository pattern shows up in almost every "clean architecture in Laravel" post. The pitch: wrap Eloquent behind a UserRepository interface so your services depend on the interface, not on Eloquent. Swap implementations freely, test with fakes, and the application is no longer "tied to the framework."
It is also one of the most over-applied patterns in the Laravel ecosystem. This post is when the repository pattern actually pays off in Laravel, when it costs more than it gives back, and what to do instead.
The Standard Repository Pattern
interface UserRepository
{
public function find(int $id): ?User;
public function save(User $user): void;
public function findByEmail(string $email): ?User;
}
final class EloquentUserRepository implements UserRepository
{
public function find(int $id): ?User
{
return UserModel::find($id);
}
public function save(User $user): void
{
$user->save();
}
public function findByEmail(string $email): ?User
{
return UserModel::where('email', $email)->first();
}
}
Bind the interface in a service provider, inject it where needed. Services depend on UserRepository, never on the Eloquent model directly.
The Promise
Three benefits are commonly cited:
- Testability. Inject a fake repository in tests; no database needed.
- Decoupling. Swap Eloquent for Doctrine, or for a different storage, without touching service code.
- Encapsulation. All user queries live in one place, not scattered across controllers and services.
Each promise is partially true. Each has a catch.
What Actually Happens
Testability. Eloquent models are testable without a repository. RefreshDatabase, factories, and SQLite in-memory let you write tests against the real models in milliseconds. The repository's testing advantage is real only if you would otherwise mock the database — and mocking the database is usually worse than using a real one.
Decoupling. Almost nobody swaps Eloquent for Doctrine. Most teams use Eloquent for the life of the application. The flexibility is paid for in every line of code; the payoff almost never arrives.
Encapsulation. This one holds up — but you can get the same encapsulation with Eloquent scopes, custom query builders, and well-named static methods on the model. You do not need a repository class to keep query logic in one place.
When Repositories Pay Off
There are real scenarios where the repository pattern justifies its cost.
You have multiple persistence stores. A UserRepository might query an aggregate of PostgreSQL, Redis, and an external API. The repository hides the composition. Eloquent alone is not enough.
You are doing strict domain-driven design. Your User is a pure PHP domain object, not an Eloquent model. The repository translates between the domain and the database. This is the Hexagonal/Ports-and-Adapters scenario; if you are not committed to it, the repository is overhead.
Query logic genuinely belongs nowhere else. A search-with-filters method might involve a dozen joins, dynamic ordering, and tenant scoping. Putting it on the model creates a fat model. A UserRepository::search($filters) keeps it contained.
Multiple read patterns for the same model. When admin views and customer views need different aggregations of the same user, repositories per use case (AdminUserRepository, CustomerUserRepository) clarify intent.
When Repositories Cost More Than They Give
Simple CRUD. If your repository is find, save, delete, you have wrapped Eloquent's API in an interface that adds zero value. Drop it.
Single application. If only one app ever uses this code, the abstraction has no consumer that benefits from the indirection.
Small team. A two-person team that knows Eloquent does not need a layer between themselves and the framework. The cognitive overhead per query is real.
Active development. Every new query type needs a new interface method, a new implementation, and a service binding. Velocity suffers.
The Practical Middle Ground
For most Laravel applications, the right call is not "use repositories everywhere" or "never use repositories." It is:
Default to using the model directly. Eloquent is a competent abstraction over the database. Calling User::find($id) from a service is not a sin.
Use scopes for reusable query logic.
final class User extends Model
{
public function scopeActive(Builder $q): Builder
{
return $q->where('status', 'active')
->where('email_verified_at', '!=', null);
}
}
// Anywhere
$activeUsers = User::active()->get();
Use query builders for complex composition.
final class UserQuery
{
public function __construct(private Builder $builder) {}
public static function for(User $user): static
{
return new static(UserModel::query()->forTenant($user->tenant_id));
}
public function active(): static
{
$this->builder->active();
return $this;
}
public function paid(): static
{
$this->builder->whereHas('subscription', fn ($q) => $q->where('status', 'active'));
return $this;
}
public function get(): Collection
{
return $this->builder->get();
}
}
UserQuery::for($admin)->active()->paid()->get();
Reach for a repository when a real scenario above applies. Multi-store, DDD, complex search, multiple read views — these are when the pattern earns its keep. Until then, the model is fine.
The "Repositories Make Testing Easier" Argument
This is the most common justification, and it is mostly wrong in Laravel specifically.
Laravel's testing tools — RefreshDatabase, model factories, SQLite in-memory — are good enough that hitting the real database in unit tests costs single-digit milliseconds. The repository pattern's testing payoff comes from replacing the database with a fake, which means your tests do not exercise the real query logic. That trade is usually bad: your code is now decoupled from the database, but your tests are also decoupled from the real database behavior.
If your testing pain is "the database is slow," the fix is faster database tests, not a repository layer. If your testing pain is "I want to test behavior, not SQL," scopes and named query methods get you most of the way without the repository.
The Real Question
When you are deciding whether to add a repository, ask: "what change does this make easier, and how likely is that change?" If the answer is "we might swap Eloquent for Doctrine," the change is so unlikely the abstraction is not justified. If the answer is "we just added a Redis cache in front of Postgres," the abstraction may be exactly right.
Most Laravel applications never need the repository pattern. The ones that do usually know why.
Working through a codebase where the repository layer has become heavier than the code it wraps? We help teams unwind unnecessary abstraction without losing the parts that genuinely earn their keep. scopeforged.com