DEV Community

Cover image for 🛠️ Build Better Laravel Apps with Service Interfaces, Providers & Requests
Tahsin Abrar
Tahsin Abrar

Posted on • Edited on

3 2 3 3 2

🛠️ Build Better Laravel Apps with Service Interfaces, Providers & Requests

🧳 Real-life Use Case: Travel Booking Service

Let’s say we’re building an API where users can book a tour. Instead of putting everything in the controller, we’ll:

  • Create a BookingService for booking logic.
  • Define a BookingServiceInterface.
  • Bind it using a Service Provider.
  • Use a Form Request class to handle validation.
  • Inject the service into a controller for actual usage.

1. Create the Interface

php artisan make:interface Services/BookingServiceInterface
Enter fullscreen mode Exit fullscreen mode

app/Services/BookingServiceInterface.php:

<?php

namespace App\Services;

interface BookingServiceInterface
{
    public function createBooking(array $data);
}
Enter fullscreen mode Exit fullscreen mode

2. Create the Service Class

php artisan make:service Services/BookingService
Enter fullscreen mode Exit fullscreen mode

app/Services/BookingService.php:

<?php

namespace App\Services;

use App\Models\Booking;

class BookingService implements BookingServiceInterface
{
    public function createBooking(array $data)
    {
        return Booking::create([
            'user_id' => $data['user_id'],
            'tour_id' => $data['tour_id'],
            'date'    => $data['date'],
            'people'  => $data['people'],
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Create a Custom Form Request for Validation

php artisan make:request StoreBookingRequest
Enter fullscreen mode Exit fullscreen mode

app/Http/Requests/StoreBookingRequest.php:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreBookingRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'user_id' => 'required|integer',
            'tour_id' => 'required|integer',
            'date'    => 'required|date',
            'people'  => 'required|integer|min:1',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Now all validation logic is separated and reusable!


4. Create a Service Provider

php artisan make:provider BookingServiceProvider
Enter fullscreen mode Exit fullscreen mode

app/Providers/BookingServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\BookingService;
use App\Services\BookingServiceInterface;

class BookingServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(BookingServiceInterface::class, BookingService::class);
    }

    public function boot()
    {
        //
    }
}
Enter fullscreen mode Exit fullscreen mode

Then register this provider in config/app.php:

App\Providers\BookingServiceProvider::class,
Enter fullscreen mode Exit fullscreen mode

5. Use the Service in a Controller

php artisan make:controller BookingController
Enter fullscreen mode Exit fullscreen mode

app/Http/Controllers/BookingController.php:

<?php

namespace App\Http\Controllers;

use App\Services\BookingServiceInterface;
use App\Http\Requests\StoreBookingRequest;

class BookingController extends Controller
{
    protected $bookingService;

    public function __construct(BookingServiceInterface $bookingService)
    {
        $this->bookingService = $bookingService;
    }

    public function store(StoreBookingRequest $request)
    {
        $booking = $this->bookingService->createBooking($request->validated());

        return response()->json([
            'message' => 'Booking created successfully!',
            'data' => $booking,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the controller is super clean — all it does is call the service with validated data.


✅ Why This Pattern Works in the Real World

  • Separation of Concerns: Controllers don’t care about validation or logic.
  • Testable: Easily mock BookingServiceInterface in tests.
  • Reusable: Same service can be used elsewhere — even in commands or jobs.
  • Maintainable: Validation logic is centralized in StoreBookingRequest.

🧪 Bonus: Mocking the Service in a Test

public function test_booking_creation()
{
    $mock = \Mockery::mock(BookingServiceInterface::class);
    $mock->shouldReceive('createBooking')->once()->andReturn(['id' => 1]);

    $this->app->instance(BookingServiceInterface::class, $mock);

    $response = $this->postJson('/api/bookings', [
        'user_id' => 1,
        'tour_id' => 2,
        'date'    => '2025-06-01',
        'people'  => 2,
    ]);

    $response->assertStatus(200);
}
Enter fullscreen mode Exit fullscreen mode

🧵 Wrapping Up

This is how you build Laravel applications that scale without becoming messy.

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

Top comments (0)

👋 Kindness is contagious

Discover this thought-provoking article in the thriving DEV Community. Developers of every background are encouraged to jump in, share expertise, and uplift our collective knowledge.

A simple "thank you" can make someone's day—drop your kudos in the comments!

On DEV, spreading insights lights the path forward and bonds us. If you appreciated this write-up, a brief note of appreciation to the author speaks volumes.

Get Started