Controllers that validate, query, calculate, send emails and return a response all in one method are easy to write and hard to live with. A small service layer keeps each part in its own place, so your code stays readable as the app grows.
When a controller is doing too much#
Watch for these signs:
- The same logic is copied between a controller, a job and an Artisan command.
- A controller method is longer than one screen.
- You can't test a business rule without making an HTTP request.
When you see them, move the business logic into a plain PHP class.
A simple service class#
A service is just a class with one clear job. Laravel's container builds it for you, so there is nothing to register.
namespace App\Services;
use App\Models\Order;
use App\Notifications\OrderPlaced;
use Illuminate\Support\Facades\DB;
class PlaceOrder
{
public function handle(array $items, int $customerId): Order
{
return DB::transaction(function () use ($items, $customerId) {
$order = Order::create(['customer_id' => $customerId]);
$order->items()->createMany($items);
$order->customer->notify(new OrderPlaced($order));
return $order;
});
}
}
Keep the controller thin#
The controller now validates the request, calls the service and returns a response. Nothing more.
public function store(StoreOrderRequest $request, PlaceOrder $placeOrder)
{
$order = $placeOrder->handle($request->validated('items'), $request->user()->id);
return new OrderResource($order);
}
Validation lives in a Form Request, the business rule lives in the service, and the response shape lives in an API Resource. Each piece can change without touching the others.
Reuse it everywhere#
Because PlaceOrder has no idea where it was called from, you can use the same class in a queued job, a console command or a test:
public function test_order_is_saved_with_items(): void
{
$order = app(PlaceOrder::class)->handle([['sku' => 'CAT-01', 'qty' => 2]], $this->customer->id);
$this->assertCount(1, $order->items);
}
Don't overdo it#
Not every model needs a service. Simple create, read, update and delete actions are fine in the controller. Add a service when logic is shared, has several steps, or needs a transaction.
Start simple. Extract a service the second time you need the same logic, not before.




