Repository vs Service: What Goes Where in Laravel

📖 2 minutes read

Repository vs Service: What Goes Where in Laravel

When refactoring Laravel applications, one common anti-pattern I see is services that directly perform database queries. This violates separation of concerns and makes code harder to test and maintain.

The Problem: Bloated Service Classes

Consider a ReportService that handles business logic for generating reports. Over time, it accumulates methods like:

class ReportService
{
    public function generateReport($projectId, $startDate, $endDate)
    {
        // Business logic here
        $data = $this->fetchReportData($projectId, $startDate, $endDate);
        // More business logic
    }

    private function fetchReportData($projectId, $startDate, $endDate)
    {
        // Direct database query
        return DB::table('reports')
            ->where('project_id', $projectId)
            ->whereBetween('created_at', [$startDate, $endDate])
            ->get();
    }

    private function getProjectSettings($projectId)
    {
        // Another direct database query
        return DB::table('project_settings')
            ->where('project_id', $projectId)
            ->first();
    }
}

This service is doing two jobs: orchestrating business logic AND querying the database.

The Solution: Move Database Queries to Repositories

Repositories should handle all database access. Services should orchestrate business logic by calling repositories.

Create a Repository:

class ReportRepository
{
    public function findReportData($projectId, $startDate, $endDate)
    {
        return DB::table('reports')
            ->where('project_id', $projectId)
            ->whereBetween('created_at', [$startDate, $endDate])
            ->get();
    }

    public function getProjectSettings($projectId)
    {
        return DB::table('project_settings')
            ->where('project_id', $projectId)
            ->first();
    }
}

Clean up the Service:

class ReportService
{
    public function __construct(
        private ReportRepository $reportRepo
    ) {}

    public function generateReport($projectId, $startDate, $endDate)
    {
        // Pure business logic - no database queries
        $data = $this->reportRepo->findReportData($projectId, $startDate, $endDate);
        $settings = $this->reportRepo->getProjectSettings($projectId);
        
        // Apply business rules, transformations, etc.
        return $this->processReportData($data, $settings);
    }
}

The Rule

Services orchestrate. Repositories query.

If your service has DB::table() or Eloquent queries, move them to a repository. Your service should read like a business workflow, not a database script.

Bonus: Testing Becomes Easier

With this separation, you can mock the repository in tests:

public function test_generates_report()
{
    $mockRepo = Mockery::mock(ReportRepository::class);
    $mockRepo->shouldReceive('findReportData')->once()->andReturn(collect([...]));
    
    $service = new ReportService($mockRepo);
    $result = $service->generateReport(1, '2024-01-01', '2024-12-31');
    
    $this->assertInstanceOf(Report::class, $result);
}

Clean separation = easier testing + clearer code architecture.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *