Eloquent already is your data abstraction. The layer you wrote on top of it is just indirection with a fancy name.

The repository pattern has infected Laravel tutorials for a decade. You will find it in official-looking guides, boot camps and GitHub starter templates, always presented as the mature, professional choice. Senior developers who use repositories in Laravel are either working in a context that genuinely justifies them (rare) or following a convention they absorbed early and never questioned (common).
🚨 High-Paying Tech Roles Available
💰 $3K–$10K/Month Remote & Onsite Opportunities
⚡ No long applications — just submit your profile in minutes
🔎 Get matched with active hiring companies
👉 Start Application (60 Seconds)

This is about the common case.
What the Pattern Was Actually Designed For
Martin Fowler defined the repository pattern in Patterns of Enterprise Application Architecture as “a mediating layer between the domain and data mapping layers.” The key phrase is data mapping. A true repository sits between your domain objects and a dedicated persistence mechanism. Your domain objects have no knowledge of the database. They are plain classes with behaviour. The mapper translates them into rows, and the repository abstracts that translator away from the caller.
This is a sound idea in a Data Mapper architecture. Doctrine, the PHP ORM that powers Symfony applications, uses this model. Your entity classes know nothing about columns or connections. You hand entities to a repository. The repository hands them to the mapper. The mapper writes SQL. Each layer has one job.
That is not what Eloquent does.
Eloquent Is an ActiveRecord ORM
Eloquent implements the ActiveRecord pattern. Your models extend Illuminate\Database\Eloquent\Model. They know about database table names, connection names and column casts. They carry their own query methods: User::find(), User::where(), User::create(). The model is the row. The model is also the query builder. It is not a domain object in the DDD sense. It is a database record with PHP methods attached.
This design choice is intentional. Taylor Otwell has always preferred thin controllers and fat models, with Eloquent doing the heavy lifting. That philosophy makes Laravel fast to work with for the vast majority of applications. It is not a compromise. It is a deliberate trade-off that works well for standard web application workloads.
The problem starts when people try to retrofit DDD patterns onto an ActiveRecord ORM and call it clean architecture.
When you write a UserRepository that calls User::find(), you have not added an abstraction. You have added a wrapper around an object that is already an abstraction over SQL. The repository does not hide the database. Eloquent does that. Your repository just adds a layer of code between the controller and Eloquent, with no meaningful separation of concerns.
The “I Will Swap Databases Later” Argument
The classic justification: “If we ever need to switch from MySQL to MongoDB, we only have to change the repository implementation. Our business logic is protected.”
This argument fails on two levels.
First, the scenario almost never happens. Most Laravel applications run on a relational database for their entire lifespan. The businesses that genuinely need to switch data sources are dealing with architectural pressures significant enough that a few interface definitions will not save them. They are rewriting data models, not swapping implementations.
Second, even if you did need to switch, your application code is already coupled to relational semantics. Your Eloquent relationships assume foreign keys. Your eager loading assumes JOINs. Your migrations define indexes on relational columns. Dropping in a MongoUserRepository does not give you a working application. It gives you a starting point for a much larger rewrite, repository pattern or not.
The interface buys you the appearance of flexibility. It does not buy you the thing itself.
What You Actually End Up With
Here is what the pattern looks like in most real Laravel codebases:
interface UserRepositoryInterface
{
public function findById(int $id): ?User;
public function findByEmail(string $email): ?User;
public function create(array $data): User;
public function update(int $id, array $data): User;
public function delete(int $id): bool;
} class EloquentUserRepository implements UserRepositoryInterface
{
public function findById(int $id): ?User
{
return User::find($id);
}
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
public function create(array $data): User
{
return User::create($data);
}
public function update(int $id, array $data): User
{
$user = User::findOrFail($id);
$user->update($data);
return $user;
}
public function delete(int $id): bool
{
return User::destroy($id) > 0;
}
}
Then you register the binding in a service provider:
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);You have now written an interface, a concrete class and a service provider binding to replicate what User::find($id) does in ten characters. Every time a requirement changes, the change cascades through four files. Every method you add to the interface must be implemented in the concrete class, mocked in tests and documented somewhere.
This is not architecture. It is ceremony.
The Testing Justification Does Not Hold Up Either
The more defensible argument for repositories is testability. If your code depends on an interface, you can inject a fake in tests without touching the database.
That argument had more weight five years ago. Laravel’s testing infrastructure has matured considerably since then.
Laravel ships with RefreshDatabase and DatabaseTransactions traits that wrap your tests in transactions and roll them back after each test. Your test suite can hit a real database and still run fast because nothing is actually persisted between cases. More relevant: Laravel 12 ships with SQLite as the default database configuration for new projects specifically because in-memory SQLite is fast enough for local development and CI pipelines.
Eloquent factories let you create test fixtures in a single line. Seeding specific states for complex scenarios is straightforward. The tooling exists to test against real database behaviour without the overhead of a persistent database between test runs.
The alternative, mocking a repository interface, produces tests that verify your mock behaves the way you told it to behave. When a query has a bug, your mocked tests go green. Your users find the bug instead. That is not the goal of a test suite.
The Service Provider Tax
There is an operational cost that rarely gets discussed: the service provider binding.
Every repository interface needs a binding. That binding lives in a service provider. In a large application, you end up with a RepositoryServiceProvider that is dozens of lines long, registering dozens of bindings, most of which wrap a single Eloquent call. The container resolves these bindings on every request. Your application bootstraps a dependency graph to support abstractions that abstract nothing.
In high-traffic applications, this is not catastrophic. But it is measurable overhead with no corresponding benefit for 95% of the methods in that provider.
There is also a cognitive cost. When a developer new to the codebase tries to understand where the application is configured, they have to hunt through service providers to understand why an interface resolves to a concrete class. This is not architectural clarity. It is a scavenger hunt.
What the Community Has Been Saying
Povilas Korop at LaravelDaily, who has been teaching Laravel patterns since the framework’s early days, published a lesson specifically titled “Repository Pattern: Why I Don’t Recommend It.” The position is not fringe. It is the conclusion that experienced Laravel practitioners reach after building enough production applications.
The pattern was popular roughly between 2014 and 2018, when Laravel was trying to prove it could support enterprise-grade architecture. A lot of tutorials from that era got treated as permanent doctrine. The framework has moved on. The tutorial content has not.
You will find zero repositories in most well-regarded open-source Laravel packages. The codebases that senior developers build for themselves, with no one watching, tend to use Eloquent directly with scopes and purpose-built query classes.
Worth noting: Laravel 12, released in February 2025, did not add repository support to the framework. It added better starter kits, updated dependencies and zero-breaking-change upgrades. The core philosophy of the framework has not shifted toward repository abstraction. If anything, the tooling consistently reinforces the opposite direction.
What You Should Actually Write
Eloquent already gives you the tools to keep query logic organised without adding a fake abstraction layer on top of it.
Query scopes let you name and chain constraints directly on your model:
// In your User model
public function scopeActive(Builder $query): void
{
$query->where('is_active', true);
}
public function scopeVerified(Builder $query): void
{
$query->whereNotNull('email_verified_at');
}
public function scopeWithRole(Builder $query, string $role): void
{
$query->where('role', $role);
} // Usage reads clearly
$admins = User::active()->verified()->withRole('admin')->paginate(20);
This is readable, discoverable and lives exactly where a developer will look for it. No extra files. No interface. No service provider binding.
Dedicated query classes work well when a query is complex, conditional or reused across multiple parts of the application. They are not repositories. They do not pretend to abstract the database. They are plain PHP classes that do one thing:
class FetchActiveUsersQuery
{
public function execute(array $filters = []): LengthAwarePaginator
{
$query = User::active()->verified()->latest();
if (!empty($filters['role'])) {
$query->withRole($filters['role']);
}
if (!empty($filters['search'])) {
$query->where('name', 'like', "%{$filters['search']}%");
}
return $query->paginate($filters['per_page'] ?? 20);
}
}This is testable with a real database or an in-memory SQLite instance. It does not require a mock. It does not require a binding. It lives in an app/Queries directory and is injected wherever the controller needs it.
Action classes handle write operations with a similar approach:
class CreateUserAction
{
public function execute(array $data): User
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
event(new UserRegistered($user));
return $user;
}
}One class, one responsibility. No interface unless you have a concrete reason for one. No service provider binding unless you need one. You can test it by calling execute() and asserting on the result.
When the Repository Pattern Is Actually Justified
The pattern earns its place when your application genuinely needs to operate across multiple data sources and the calling code must be isolated from which source it is querying. Not “we might switch databases someday” but “we currently query a PostgreSQL database for user records and a third-party CRM API for contact data, and the caller should not care which is which.”
If that is your situation, the interface is doing real work. Use it.
If your situation is a standard Laravel application with one relational database, do not reach for it. The interface is not doing work. It is doing paperwork.
The Hidden Cost Is Complexity
Every abstraction layer you add to a codebase is a cost that someone else pays later.
When a new developer joins your team and tries to trace a query from a controller to the database, they hit your repository, then your interface, then your service provider binding, then finally Eloquent. Three files of indirection that convey no useful information about what the query is or why it exists.
That developer will then replicate the pattern elsewhere because it looks like the established way things work here. They will create repositories for every model. They will add bindings to the service provider. They will write tests that mock the interfaces. The pattern compounds.
The codebases that are easiest to work in tend to be the ones that do not solve problems that do not exist. If your query logic needs a name, give it a scope. If it needs isolation, give it a query class. If it needs to be swappable, give it an interface.
Most of the time, none of that is true. User::where('email', $email)->first() is already clear. It does not need a wrapper. It needs to be called directly.
The next time you open a new UserRepositoryInterface.php file, close it and ask yourself what problem it is solving. If the answer involves the word "someday," that is your answer.
Thank you for being a part of the community
Before you go:

👉 Be sure to clap and follow the writer ️👏️️
👉 CodeToDeploy Tech Community is live on Discord — Join now!
Disclosure: This post includes affiliate and partnership links.


