Introduction
When you are new to PHP or Laravel, it is very easy to put too much code inside controllers.
At first, this feels normal.
You create a controller method, receive a request, validate the data, create a model, send an email, update another table, dispatch a notification, and return a response.
It works.
But after some time, your controller becomes huge. One method may become 100 lines long. Then 200 lines. Then every small change becomes risky.
This is where the Service Pattern becomes useful.
The Service Pattern helps you move business logic into separate classes called services.
Instead of writing everything inside the controller, you create a service class that handles a specific part of your application logic.
The controller stays small.
The service handles the real work.
Your code becomes easier to read, test, and maintain.
The Problem: Fat Controllers
Let’s imagine you have a simple Laravel application where users can book a pet grooming appointment.
A beginner might write something like this inside a controller:
namespace App\Http\Controllers;
use App\Models\Appointment;
use App\Models\User;
use App\Notifications\AppointmentBookedNotification;
use Illuminate\Http\Request;
class AppointmentController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'pet_id' => ['required', 'exists:pets,id'],
'groomer_id' => ['required', 'exists:users,id'],
'appointment_date' => ['required', 'date'],
'service_type' => ['required', 'string'],
]);
$appointment = Appointment::create([
'pet_id' => $validated['pet_id'],
'groomer_id' => $validated['groomer_id'],
'appointment_date' => $validated['appointment_date'],
'service_type' => $validated['service_type'],
'status' => 'pending',
]);
$groomer = User::find($validated['groomer_id']);
$groomer->notify(new AppointmentBookedNotification($appointment));
return redirect()
->route('appointments.show', $appointment)
->with('success', 'Appointment booked successfully.');
}
}This code is not terrible. For a small feature, it works.
But the controller is doing too many things:
It validates the request
It creates the appointment
It decides the default status
It finds the groomer
It sends a notification
It returns a response
Now imagine the booking logic becomes bigger.
Maybe you need to check if the groomer is available.
Maybe you need to prevent duplicate appointments.
Maybe you need to calculate the price.
Maybe you need to apply a discount.
Maybe you need to send an email to the customer.
Maybe you need to create a payment record.
If you keep adding everything to the controller, the controller becomes messy very quickly.
This is called a fat controller.
The Service Pattern helps solve this problem.
What Is the Service Pattern?
The Service Pattern is a way to organize business logic into separate classes.
A service class is a normal PHP class that performs a specific job in your application.
For example:
AppointmentBookingService
PaymentService
InvoiceService
UserRegistrationService
NotificationServiceEach service should focus on a specific responsibility.
In simple words:
A service is a class that contains business logic that does not belong directly in a controller or model.
The controller should handle HTTP-related things.
The model should represent data and relationships.
The service should handle business rules and application logic.
What Is Business Logic?
Business logic means the rules of your application.
For example, in a pet grooming application, business logic could be:
A groomer cannot have two appointments at the same time
A customer must pay before booking a premium grooming package
A puppy grooming session has a different price than an adult dog grooming session
A canceled appointment cannot be paid
A discount code can only be used once
A customer receives a notification after booking
These rules are not just database operations. They describe how your application should behave.
That logic should not be hidden inside a huge controller method.
A service class is a good place for it.
Basic Example of a Service Class
Let’s create a simple service for booking an appointment.
In Laravel, you can create this manually:
app/Services/AppointmentBookingService.phpExample:
namespace App\Services;
use App\Models\Appointment;
use App\Models\User;
use App\Notifications\AppointmentBookedNotification;
class AppointmentBookingService
{
public function book(array $data): Appointment
{
$appointment = Appointment::create([
'pet_id' => $data['pet_id'],
'groomer_id' => $data['groomer_id'],
'appointment_date' => $data['appointment_date'],
'service_type' => $data['service_type'],
'status' => 'pending',
]);
$groomer = User::find($data['groomer_id']);
$groomer?->notify(new AppointmentBookedNotification($appointment));
return $appointment;
}
}Now the controller becomes much cleaner:
namespace App\Http\Controllers;
use App\Services\AppointmentBookingService;
use Illuminate\Http\Request;
class AppointmentController extends Controller
{
public function store(Request $request, AppointmentBookingService $appointmentBookingService)
{
$validated = $request->validate([
'pet_id' => ['required', 'exists:pets,id'],
'groomer_id' => ['required', 'exists:users,id'],
'appointment_date' => ['required', 'date'],
'service_type' => ['required', 'string'],
]);
$appointment = $appointmentBookingService->book($validated);
return redirect()
->route('appointments.show', $appointment)
->with('success', 'Appointment booked successfully.');
}
}This is already better.
The controller now does what a controller should do:
Receive the request
Validate the input
Call the service
Return the response
The service does the business work.
Why Is This Better?
The Service Pattern makes your code cleaner because every class has a clearer responsibility.
The controller is no longer responsible for knowing every detail about booking an appointment.
If the booking process changes later, you update the service.
For example, imagine you want to add a price calculation:
$price = $this->calculatePrice($data['service_type']);You can add that logic inside the service without making the controller bigger.
If you want to send another notification, you also add it inside the service.
If you want to write a test for the booking logic, you can test the service directly.
This makes the application easier to grow.
A Better Service Example
The first service example works, but we can improve it.
Let’s add a simple availability check.
namespace App\Services;
use App\Models\Appointment;
use App\Models\User;
use App\Notifications\AppointmentBookedNotification;
use Exception;
class AppointmentBookingService
{
public function book(array $data): Appointment
{
$this->ensureGroomerIsAvailable(
groomerId: $data['groomer_id'],
appointmentDate: $data['appointment_date'],
);
$appointment = Appointment::create([
'pet_id' => $data['pet_id'],
'groomer_id' => $data['groomer_id'],
'appointment_date' => $data['appointment_date'],
'service_type' => $data['service_type'],
'status' => 'pending',
]);
$groomer = User::find($data['groomer_id']);
$groomer?->notify(new AppointmentBookedNotification($appointment));
return $appointment;
}
protected function ensureGroomerIsAvailable(int $groomerId, string $appointmentDate): void
{
$alreadyBooked = Appointment::query()
->where('groomer_id', $groomerId)
->where('appointment_date', $appointmentDate)
->exists();
if ($alreadyBooked) {
throw new Exception('The selected groomer is not available at this time.');
}
}
}Now the service contains an actual business rule:
A groomer cannot have two appointments at the same time.
This is exactly the kind of logic that should not live inside the controller.
Handling Errors in the Controller
Because the service may throw an exception, the controller can handle it:
namespace App\Http\Controllers;
use App\Services\AppointmentBookingService;
use Exception;
use Illuminate\Http\Request;
class AppointmentController extends Controller
{
public function store(Request $request, AppointmentBookingService $appointmentBookingService)
{
$validated = $request->validate([
'pet_id' => ['required', 'exists:pets,id'],
'groomer_id' => ['required', 'exists:users,id'],
'appointment_date' => ['required', 'date'],
'service_type' => ['required', 'string'],
]);
try {
$appointment = $appointmentBookingService->book($validated);
} catch (Exception $exception) {
return back()
->withInput()
->withErrors([
'appointment_date' => $exception->getMessage(),
]);
}
return redirect()
->route('appointments.show', $appointment)
->with('success', 'Appointment booked successfully.');
}
}This keeps things clean.
The service decides if the appointment can be booked.
The controller decides how to show the error to the user.
That separation is important.
Service Pattern in Plain PHP
The Service Pattern is not only for Laravel. It works in plain PHP too.
Example:
class PriceCalculatorService
{
public function calculate(string $serviceType): int
{
return match ($serviceType) {
'basic_grooming' => 30,
'full_grooming' => 60,
'premium_grooming' => 90,
default => 0,
};
}
}Usage:
$priceCalculator = new PriceCalculatorService();
$price = $priceCalculator->calculate('full_grooming');
echo $price;This is a service because it performs a specific business operation: calculating a price.
It does not need to be a Laravel class. It is just a PHP class with a clear job.
Service Pattern vs Controller
A controller should not contain too much business logic.
A controller should mostly answer this question:
What should happen when this HTTP request comes in?
A service should answer this question:
How should this business operation actually work?
For example:
class RegisterUserController
{
public function store()
{
// Validate request
// Call UserRegistrationService
// Return response
}
}And:
class UserRegistrationService
{
public function register(array $data)
{
// Create user
// Assign role
// Send welcome email
// Create default settings
}
}The controller handles the web request.
The service handles the registration process.
Service Pattern vs Model
A common beginner mistake is putting too much logic inside models.
For example, you might be tempted to put booking logic inside the Appointment model:
Appointment::book($data);This can be okay for very small logic, but it can become messy when the process involves multiple models.
For example, booking an appointment may involve:
Appointment
Pet
User
Payment
Notification
Discount
Calendar availability
That is too much responsibility for one model.
A model should usually represent data, relationships, scopes, accessors, mutators, and simple model-specific behavior.
A service is better when the logic coordinates multiple parts of the application.
Service Pattern vs Action Pattern
The Service Pattern and Action Pattern are similar, but they are not exactly the same.
An Action usually represents one specific operation.
Examples:
CreateAppointmentAction
CancelAppointmentAction
PublishArticleAction
RegisterUserActionA Service can contain a group of related operations.
Examples:
AppointmentService
PaymentService
InvoiceService
UserServiceA service might have methods like:
class AppointmentService
{
public function book(array $data): Appointment
{
//
}
public function cancel(Appointment $appointment): void
{
//
}
public function reschedule(Appointment $appointment, string $newDate): Appointment
{
//
}
}An action usually has one main method:
class BookAppointmentAction
{
public function execute(array $data): Appointment
{
//
}
}Both approaches can be good.
For beginners, the Service Pattern is often easier to understand because one service can group related logic.
For larger projects, the Action Pattern can be cleaner because each class has only one job.
When Should You Use a Service?
You should use a service when your controller or model starts doing too much.
Good examples include:
Booking an appointment
Registering a user
Creating an invoice
Processing a payment
Sending several notifications
Importing data from a file
Generating reports
Calculating prices
Applying discounts
Publishing an article
Creating an order
Canceling a subscription
A simple rule:
If a piece of logic has multiple steps, touches multiple models, or may be reused later, it is probably a good candidate for a service.
When You Do Not Need a Service
You do not need to create a service for every tiny thing.
For example, this probably does not need a service:
User::find($id);Or this:
Post::latest()->paginate();Creating services for very simple CRUD logic can make your application more complicated for no reason.
Bad example:
class UserService
{
public function getAllUsers()
{
return User::all();
}
public function findUser(int $id)
{
return User::find($id);
}
}This does not add much value.
It only wraps Eloquent methods without improving the code.
A service should contain meaningful application logic, not just duplicate basic model methods.
How to Name Service Classes
Naming is important.
A service name should describe what the service does.
Good names:
AppointmentBookingService
InvoiceGenerationService
PaymentProcessingService
UserRegistrationService
ArticlePublishingService
PriceCalculationServiceWeak names:
UserHelper
CommonService
GeneralService
MainService
DataServiceAvoid names like Helper, Manager, or CommonService unless there is a very clear reason.
These names usually become messy because developers keep adding unrelated methods to them.
Where Should Services Live in Laravel?
A common place is:
app/ServicesExample:
app/
├── Http/
│ └── Controllers/
├── Models/
├── Services/
│ └── AppointmentBookingService.phpFor larger applications, you may organize services by domain:
app/
├── Services/
│ ├── Appointments/
│ │ └── AppointmentBookingService.php
│ ├── Payments/
│ │ └── PaymentProcessingService.php
│ └── Articles/
│ └── ArticlePublishingService.phpBoth are fine.
For small and medium projects, app/Services is enough.
For bigger projects, grouping by feature or domain is cleaner.
Dependency Injection with Services
Laravel can automatically inject service classes into your controller methods or constructors.
Example:
public function store(
Request $request,
AppointmentBookingService $appointmentBookingService
) {
//
}You do not need to manually write:
$appointmentBookingService = new AppointmentBookingService();Laravel’s service container can create the service object for you.
This is useful because your service may also need other dependencies.
Example:
namespace App\Services;
use App\Services\PriceCalculationService;
class AppointmentBookingService
{
public function __construct(
protected PriceCalculationService $priceCalculationService
) {}
public function book(array $data): Appointment
{
$price = $this->priceCalculationService->calculate($data['service_type']);
// Continue booking...
}
}Laravel can resolve this automatically.
A More Realistic Example
Let’s build a more realistic appointment booking service.
namespace App\Services;
use App\Models\Appointment;
use App\Models\User;
use App\Notifications\AppointmentBookedNotification;
use Exception;
use Illuminate\Support\Facades\DB;
class AppointmentBookingService
{
public function __construct(
protected PriceCalculationService $priceCalculationService
) {}
public function book(array $data): Appointment
{
return DB::transaction(function () use ($data) {
$this->ensureGroomerIsAvailable(
groomerId: $data['groomer_id'],
appointmentDate: $data['appointment_date'],
);
$price = $this->priceCalculationService->calculate(
serviceType: $data['service_type'],
);
$appointment = Appointment::create([
'pet_id' => $data['pet_id'],
'groomer_id' => $data['groomer_id'],
'appointment_date' => $data['appointment_date'],
'service_type' => $data['service_type'],
'price' => $price,
'status' => 'pending',
]);
$groomer = User::find($data['groomer_id']);
$groomer?->notify(new AppointmentBookedNotification($appointment));
return $appointment;
});
}
protected function ensureGroomerIsAvailable(int $groomerId, string $appointmentDate): void
{
$alreadyBooked = Appointment::query()
->where('groomer_id', $groomerId)
->where('appointment_date', $appointmentDate)
->exists();
if ($alreadyBooked) {
throw new Exception('The selected groomer is not available at this time.');
}
}
}And the price service:
namespace App\Services;
class PriceCalculationService
{
public function calculate(string $serviceType): int
{
return match ($serviceType) {
'basic_grooming' => 30,
'full_grooming' => 60,
'premium_grooming' => 90,
default => 0,
};
}
}This is much better than putting all of this inside the controller.
The controller remains simple, and the business logic is easier to understand.
Why Use a Database Transaction?
In the realistic example, we used:
DB::transaction(function () {
//
});A transaction means that all database changes inside the function should succeed together.
If something fails, Laravel can roll back the changes.
This is important when a service performs multiple database operations.
For example, imagine you create an appointment, create a payment record, and reduce available slots.
If the payment record fails but the appointment was already created, your data becomes inconsistent.
A transaction helps protect your application from that.
Testing a Service
Another big benefit of the Service Pattern is testing.
Because the booking logic is inside a service, you can test it directly.
Example:
use App\Models\Pet;
use App\Models\User;
use App\Services\AppointmentBookingService;
it('can book an appointment', function () {
$pet = Pet::factory()->create();
$groomer = User::factory()->create();
$service = app(AppointmentBookingService::class);
$appointment = $service->book([
'pet_id' => $pet->id,
'groomer_id' => $groomer->id,
'appointment_date' => now()->addDay()->format('Y-m-d H:i:s'),
'service_type' => 'full_grooming',
]);
expect($appointment)
->pet_id->toBe($pet->id)
->groomer_id->toBe($groomer->id)
->service_type->toBe('full_grooming')
->status->toBe('pending');
});This test focuses on the booking logic.
You do not need to test the whole controller just to know if appointment booking works.
Common Mistakes Beginners Make
The first common mistake is creating services for everything.
Not every model needs a service. Not every query needs a service. Use services when they make your code cleaner.
The second mistake is making one giant service.
For example:
class UserService
{
public function register()
{
//
}
public function login()
{
//
}
public function updateProfile()
{
//
}
public function deleteAccount()
{
//
}
public function sendNewsletter()
{
//
}
public function exportReports()
{
//
}
}This can become too large.
If a service becomes huge, split it into smaller services or actions.
The third mistake is mixing unrelated logic.
A payment service should not contain article publishing logic.
An appointment service should not contain invoice export logic.
A user service should not contain product search logic.
Keep services focused.
The fourth mistake is using services as helpers.
A service should not become a random collection of static methods.
Avoid this:
class HelperService
{
public static function formatDate()
{
//
}
public static function cleanString()
{
//
}
public static function calculatePrice()
{
//
}
}This is not a good service design.
Best Practices
Use clear names.
A good service name explains the business operation clearly.
Keep controllers thin.
The controller should validate, call the service, and return a response.
Keep services focused.
A service should handle related logic, not everything in the application.
Do not overuse services.
If Eloquent or a simple controller method is enough, do not add unnecessary abstraction.
Use dependency injection.
Let Laravel inject services instead of manually creating them with new.
Use transactions for multi-step database operations.
If a service creates or updates multiple records, consider wrapping the logic in a transaction.
Keep validation in Form Requests when possible.
In Laravel, request validation usually belongs in a Form Request, not inside the service.
Keep HTTP logic out of services.
A service should not usually return redirects or views. That is the controller’s job.
Bad:
return redirect()->route('appointments.index');Good:
return $appointment;The service returns useful data, and the controller decides how to respond.
Final Simple Structure
A good beginner-friendly structure looks like this:
Controller
↓
Form Request Validation
↓
Service
↓
Models / Database / Notifications / Other ServicesExample:
AppointmentController
↓
StoreAppointmentRequest
↓
AppointmentBookingService
↓
Appointment, User, Notification, PriceCalculationServiceThis structure is simple, clean, and practical.
Conclusion
The Service Pattern is one of the most useful patterns for PHP and Laravel developers.
It helps you move business logic out of controllers and into dedicated classes. This makes your code easier to read, easier to test, and easier to maintain.
As a beginner, you do not need to use services everywhere. Start using them when your controller becomes too large, when a process has multiple steps, or when logic needs to be reused.
A good service should have a clear name, a clear responsibility, and should return useful results instead of handling HTTP responses.
The basic idea is simple:
Controllers handle requests. Services handle business logic.
Once you understand this idea, you can start writing cleaner Laravel applications immediately.