Service Classes in Laravel: Clean Architecture and Scalability

✍️ Introduction

In Laravel, it’s convenient to put business logic into controllers or models. But what should you do when the logic starts to grow? The right solution is to move it into dedicated service classes. This approach follows the Single Responsibility Principle (SRP) and helps make your code scalable and easy to test.


🧱 Structure of a Service Class

Let’s take a look at a basic example of a service class responsible for creating an order:

app/
β”œβ”€β”€ Services/
β”‚   └── OrderService.php
namespace App\Services;

use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function create(array $data, User $user): Order
    {
        return DB::transaction(function () use ($data, $user) {
            $order = new Order();
            $order->user_id = $user->id;
            $order->total = $data['total'];
            $order->status = 'new';
            $order->save();

            // Here you could dispatch events, send emails, etc.

            return $order;
        });
    }
}

πŸš€ Using It in a Controller

namespace App\Http\Controllers;

use App\Http\Requests\OrderRequest;
use App\Services\OrderService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;

class OrderController extends Controller
{
    public function __construct(
        protected OrderService $orderService
    ) {}

    public function store(OrderRequest $request): JsonResponse
    {
        $order = $this->orderService->create($request->validated(), Auth::user());

        return response()->json([
            'message' => 'Order created successfully.',
            'order' => $order,
        ]);
    }
}

βœ… Why This Approach Rocks

  • Cleaner architecture: Controllers become slim and act as entry points, not logic holders.
  • Easy testing: Services can be tested in isolation with mocked dependencies.
  • Flexibility: Services are easier to extend or modify without touching the controller.
  • Reusability: You can call the same service from REST APIs, Artisan commands, or queued jobs.

πŸ§ͺ How to Unit Test the Service

use App\Models\User;
use App\Services\OrderService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class OrderServiceTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_creates_an_order()
    {
        $user = User::factory()->create();
        $data = ['total' => 1000];

        $service = new OrderService();
        $order = $service->create($data, $user);

        $this->assertDatabaseHas('orders', [
            'user_id' => $user->id,
            'total' => 1000,
        ]);
    }
}

πŸ”š Conclusion

Service classes in Laravel aren’t overengineering β€” they’re a smart and maintainable way to structure your business logic. By adopting this pattern, your Laravel applications become cleaner, more modular, and ready for future growth.

Leave a Reply

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