Updated 19 June 2026
If you’ve worked on a Laravel application for more than a few months, you’ve probably seen a controller that looks like this:
|
1 2 3 4 5 6 7 8 |
public function index() { $posts = Post::where('status', 'published') ->orderBy('created_at', 'desc') ->with('author', 'comments') ->paginate(10); returnview('posts.index', compact('posts')); } |
It works. It’s also fine — for a while. Then the same query shows up in three other controllers, a teammate adds a slightly different version of it in a fourth, and six months later nobody is sure which one is “correct.” This is the moment most teams start asking about the repository pattern.
This post walks through what the repository pattern actually is, why it matters in a Laravel codebase specifically, and how to implement it cleanly — with real code you can drop into a project today.
Eloquent is one of Laravel’s best features. It’s also very easy to lean on too heavily, because it’s so convenient. Controllers, jobs, and even Blade views end up calling Model::where(...) directly. Over time this creates three concrete problems:
Post::where(...) directly usually means spinning up a database, because there’s no seam to inject a fake.The repository pattern addresses this by inserting one layer between your application logic and your data source.
At its core, the repository pattern says: don’t talk to your data source directly — talk to a repository instead.
A repository is a class that centralizes data access logic behind a clean, predictable interface. Your controllers and services ask the repository for data (“give me all published posts”) without knowing or caring whether that data comes from Eloquent, a third-party API, a cache, or a flat file.
The diagram below shows the structural difference this creates.

Notice what changed on the right side: the controller no longer talks to Eloquent at all. It depends on an interface — a contract — and Laravel’s service container decides which concrete class fulfills that contract at runtime.
Let’s build this for a Post model.
Create an interface that describes what operations are available, without saying how they’re implemented.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// app/Repositories/Contracts/PostRepositoryInterface.php namespace App\Repositories\Contracts; use App\Models\Post; use Illuminate\Support\Collection; interface PostRepositoryInterface { public function all(): Collection; public function find(int $id): ?Post; public function findPublished(): Collection; public function create(array $data): Post; public function update(Post $post, array $data): bool; public function delete(Post $post): bool; } |
This interface is the most important file in the whole pattern. It’s the seam that everything else hangs off of.
This is where your actual Eloquent queries live — and only here.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// app/Repositories/Eloquent/EloquentPostRepository.php namespace App\Repositories\Eloquent; use App\Models\Post; use App\Repositories\Contracts\PostRepositoryInterface; use Illuminate\Support\Collection; class EloquentPostRepository implements PostRepositoryInterface { public function all(): Collection { return Post::with('author')->latest()->get(); } public function find(int $id): ?Post { return Post::find($id); } public function findPublished(): Collection { return Post::where('status', 'published') ->with('author', 'comments') ->latest() ->get(); } public function create(array $data): Post { return Post::create($data); } public function update(Post $post, array $data): bool { return $post->update($data); } public function delete(Post $post): bool { return $post->delete(); } } |
Nothing fancy here — it’s the same Eloquent code you’d normally write, just relocated.
Laravel needs to know which concrete class to hand over whenever something asks for PostRepositoryInterface. You do that with a binding, typically in a dedicated service provider.
|
1 |
php artisan make:provider RepositoryServiceProvider |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// app/Providers/RepositoryServiceProvider.php namespace App\Providers; use App\Repositories\Contracts\PostRepositoryInterface; use App\Repositories\Eloquent\EloquentPostRepository; use Illuminate\Support\ServiceProvider; class RepositoryServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind( PostRepositoryInterface::class, EloquentPostRepository::class ); } } |
Don’t forget to register the provider in bootstrap/providers.php (Laravel 11+) or config/app.php (earlier versions).
The diagram below shows how this binding connects everything at runtime.

Now your controller depends on the interface, not Eloquent. Laravel’s service container automatically injects the bound implementation.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// app/Http/Controllers/PostController.php namespace App\Http\Controllers; use App\Repositories\Contracts\PostRepositoryInterface; use Illuminate\Http\Request; class PostController extends Controller { public function __construct( protected PostRepositoryInterface $posts ) {} public function index() { $posts = $this->posts->findPublished(); return view('posts.index', compact('posts')); } public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string', ]); $post = $this->posts->create($validated); return redirect()->route('posts.show', $post)->with('success', 'Post created.'); } } |
The controller no longer knows that Eloquent exists. If you swapped the data source to an external CMS API tomorrow, this file wouldn’t change at all.
This is where the pattern usually earns its keep. Because PostController depends on an interface, you can hand it a fake repository in tests instead of hitting a real database.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// tests/Unit/PostControllerTest.php namespace Tests\Unit; use App\Models\Post; use App\Repositories\Contracts\PostRepositoryInterface; use Tests\TestCase; use Mockery; class PostControllerTest extends TestCase { public function test_index_displays_published_posts(): void { $fakePosts = collect([ new Post(['title' => 'First Post', 'status' => 'published']), new Post(['title' => 'Second Post', 'status' => 'published']), ]); $mockRepo = Mockery::mock(PostRepositoryInterface::class); $mockRepo->shouldReceive('findPublished')->once()->andReturn($fakePosts); $this->app->instance(PostRepositoryInterface::class, $mockRepo); $response = $this->get(route('posts.index')); $response->assertOk(); $response->assertSee('First Post'); } } |
No database, no factories, no seeders — just a fast, isolated test that verifies the controller’s actual responsibility: taking data from the repository and rendering it.
The repository pattern isn’t free, and it isn’t always the right call. A few things worth knowing before you adopt it everywhere:
find/create/update/delete, a repository can feel like ceremony for its own sake. Some teams build a generic base repository to cut down on repetition here.A reasonable middle ground a lot of teams land on: use repositories for models with real query complexity or multiple data-access patterns, and let trivial models use Eloquent directly.
The repository pattern in Laravel isn’t about avoiding Eloquent — it’s about giving your business logic a stable contract to depend on, instead of a concrete ORM implementation. That one decision is what makes your code easier to test, easier to refactor, and easier to reason about as the codebase grows.
Start small: pick the one model in your app whose queries are duplicated the most, wrap it in a repository, and feel out whether the pattern earns its place in your project before rolling it out everywhere.
If you have more details or questions, you can reply to the received confirmation email.
Back to Home
Be the first to comment.