When I first heard about Repository Pattern, I thought it was just developers trying to look smart. “Laravel already has Eloquent. Why add another layer?”
After using it in several real projects, my perspective changed. It’s not about better or worse—it’s about knowing when it actually solves problems.
What’s Repository Pattern Anyway?
Repository Pattern sits between your controller and model. Instead of writing queries directly in controllers, you centralize them in repository classes with descriptive method names.
Without Repository
class ProductController extends Controller
{
public function index()
{
$products = Product::where('stock', '>', 0)
->where('status', 'active')
->with('category')
->latest()
->paginate(20);
return view('products.index', compact('products'));
}
}
With Repository
// ProductRepository.php
class ProductRepository
{
protected $model;
public function __construct(Product $model)
{
$this->model = $model;
}
public function getAvailableProducts()
{
return $this->model->where('stock', '>', 0)
->where('status', 'active')
->with('category')
->latest()
->paginate(20);
}
}
// ProductController.php
class ProductController extends Controller
{
protected $productRepo;
public function __construct(ProductRepository $productRepo)
{
$this->productRepo = $productRepo;
}
public function index()
{
$products = $this->productRepo->getAvailableProducts();
return view('products.index', compact('products'));
}
}
Controller stays clean, query logic stays centralized.
How I Set It Up
I keep it simple—no interfaces, no service providers. Laravel’s dependency injection handles everything automatically.
// app/Repositories/ProductRepository.php
namespace App\Repositories;
use App\Models\Product;
class ProductRepository
{
protected $model;
public function __construct(Product $model)
{
$this->model = $model;
}
public function all()
{
return $this->model->latest()->get();
}
public function find($id)
{
return $this->model->findOrFail($id);
}
public function create(array $data)
{
return $this->model->create($data);
}
public function update($id, array $data)
{
$product = $this->find($id);
$product->update($data);
return $product;
}
public function delete($id)
{
return $this->model->destroy($id);
}
// Custom queries
public function getAvailableProducts()
{
return $this->model->where('stock', '>', 0)
->where('status', 'active')
->latest()
->paginate(20);
}
public function lowStockProducts($threshold = 10)
{
return $this->model->where('stock', '<', $threshold)
->where('status', 'active')
->get();
}
}
Just inject it into controllers:
class ProductController extends Controller
{
protected $productRepo;
public function __construct(ProductRepository $productRepo)
{
$this->productRepo = $productRepo;
}
public function index()
{
$products = $this->productRepo->getAvailableProducts();
return view('products.index', compact('products'));
}
public function store(Request $request)
{
$product = $this->productRepo->create($request->validated());
return redirect()->route('products.index');
}
}
Real Project Experience
POS System
Context: Point of Sale for retail stores. Products, transactions, inventory tracking.
What I did:
- Created repositories for Product, Transaction, Inventory
- Complex queries that get called from multiple places
Real example:
// TransactionRepository.php
public function getTodaysSales()
{
return $this->model->whereDate('created_at', today())
->sum('total_amount');
}
public function getTopSellingProducts($limit = 10)
{
return $this->model->with('items.product')
->whereDate('created_at', '>=', now()->subDays(30))
->get()
->flatMap->items
->groupBy('product_id')
->map->sum('quantity')
->sortDesc()
->take($limit);
}
These methods get called from the dashboard, reports section, and API endpoints. Without repository, I’d be copy-pasting this query everywhere.
The verdict: Extremely useful. Kept code DRY and made maintenance way easier.
CRM System
Context: Customer Relationship Management. Customers, leads, deals, activities tracking.
What I did:
- Repositories for all major models
- Complex filtering logic
Example:
// CustomerRepository.php
public function filterCustomers(array $filters)
{
$query = $this->model->query();
if (isset($filters['status'])) {
$query->where('status', $filters['status']);
}
if (isset($filters['segment'])) {
$query->where('segment', $filters['segment']);
}
if (isset($filters['search'])) {
$query->where('name', 'like', "%{$filters['search']}%");
}
return $query->latest()->paginate(20);
}
public function getActiveCustomers()
{
return $this->model->where('status', 'active')
->whereHas('deals', function($q) {
$q->whereDate('last_activity', '>=', now()->subDays(30));
})
->get();
}
The verdict: Worth it. Complex filters and reusable queries made development faster.
HRIS - Adding Service Layer
Context: HR Information System. Payroll, attendance tracking, leave management.
What I did:
- Repository + Service Layer
- Complex business logic involving multiple models
Why the service layer? Because there’s a lot of business logic beyond just database queries.
// AttendanceService.php
class AttendanceService
{
protected $attendanceRepo;
protected $employeeRepo;
protected $leaveRepo;
public function __construct(
AttendanceRepository $attendanceRepo,
EmployeeRepository $employeeRepo,
LeaveRepository $leaveRepo
) {
$this->attendanceRepo = $attendanceRepo;
$this->employeeRepo = $employeeRepo;
$this->leaveRepo = $leaveRepo;
}
public function checkIn($employeeId)
{
// Verify employee exists
$employee = $this->employeeRepo->find($employeeId);
// Check for duplicate check-in
$existing = $this->attendanceRepo->getTodayAttendance($employeeId);
if ($existing) {
throw new \Exception('Already checked in today');
}
// Verify not on leave
$onLeave = $this->leaveRepo->isOnLeaveToday($employeeId);
if ($onLeave) {
throw new \Exception('Employee is on leave');
}
// Record attendance
$attendance = $this->attendanceRepo->create([
'employee_id' => $employeeId,
'check_in' => now(),
'status' => $this->calculateStatus(now())
]);
// Handle notifications, logging, etc.
return $attendance;
}
private function calculateStatus($checkInTime)
{
$workStart = Carbon::parse('09:00');
return $checkInTime->gt($workStart) ? 'late' : 'on_time';
}
}
Repository handles data access, Service handles complex business logic.
The verdict: Service layer became essential for business logic involving validation and multiple models.
When to Use Repository Pattern
✅ Use Repository When:
Same query appears in multiple places
// Instead of repeating this everywhere
Product::where('stock', '>', 0)->where('status', 'active')->get()
// Better to centralize
$this->productRepo->getAvailableProducts()
Working on medium-to-large projects
Projects with 10+ models usually benefit from repositories.
Dealing with complex filters
Queries with multiple conditional filters are cleaner in repositories.
❌ Skip Repository When:
Building small or personal projects
Blogs, portfolios—Eloquent in controllers works fine.
Doing simple CRUD
If it’s just User::all() and User::create(), don’t overthink it.
Racing against tight deadlines
Shipping fast matters more than perfect architecture.
Practical Tips
1. Don’t Create a Method for Every Query
// ❌ Avoid this
public function getProductsByName($name) { }
public function getProductsByCategory($category) { }
public function getProductsByStatus($status) { }
// ✅ Do this instead
public function filterProducts(array $filters)
{
$query = $this->model->query();
foreach ($filters as $key => $value) {
$query->where($key, $value);
}
return $query->get();
}
2. Return Collections, Not Query Builders
// ❌ Leaky abstraction
public function getActive()
{
return $this->model->where('status', 'active'); // Returns query builder
}
// ✅ Proper encapsulation
public function getActive()
{
return $this->model->where('status', 'active')->get(); // Returns collection
}
3. Start Simple
Don’t rush to create repositories for every model. Start with the ones you use most frequently.
Bottom Line
Repository Pattern isn’t a “best practice” you must follow. It’s a tool—useful in certain situations, overkill in others.
Start with Eloquent in controllers. For small projects, that’s totally fine.
Refactor to Repository when needed. When queries start duplicating or your project scales, consider repositories.
Add Service Layer if necessary. For complex business logic spanning multiple models, services help a lot.
The key: Choose patterns based on the problems you’re solving, not because “it’s what pros do”.
Repository Pattern isn’t better. It’s just different. Use it wisely.