Laravel refactoring into action classes helps you shrink fat controllers. Learn to build a clean service layer that makes your PHP code easier to test.

I remember staring at a UserController@store method that was nearly 150 lines long. It was handling validation, database inserts, sending welcome emails, and even triggering a Slack notification. It was a nightmare to debug, and adding a simple feature usually meant breaking something else.
If your controllers are doing more than receiving a request and returning a response, you’re likely dealing with "fat controller" syndrome. Let's look at how Laravel refactoring using action classes can bring sanity back to your codebase.
Early in my career, I thought putting logic in controllers was just "how Laravel worked." After all, the framework makes it so easy to write everything inside a single method. But as soon as you need that same registration logic in a CLI command or a background job, you’re stuck. You end up copy-pasting code or—worse—manually triggering controller methods, which is a massive anti-pattern.
When you start designing a clean service layer in Laravel without over-abstraction, you realize that the controller's only job is to be an entry point. It should validate the request, call the right service or action, and return a response.

An "Action" class is essentially a single-purpose class that performs one specific business process. I prefer the "Single Action" pattern, where each class has an __invoke method. It keeps the syntax clean and makes dependency injection a breeze.
Here is what a typical fat controller looks like before we touch it:
PHPpublic function store(Request $request) { $validated = $request->validate([...]); $user = User::create($validated); #6A9955">// ... 50 lines of logic ... Mail::to($user)->send(new WelcomeEmail($user)); return response()->json($user, 201); }
To clean this up, we create an action class: app/Actions/CreateNewUser.php.
PHPnamespace App\Actions; use App\Models\User; class CreateNewUser { public function __invoke(array $data): User { $user = User::create($data); #6A9955">// Business logic goes here return $user; } }
Now, your controller becomes remarkably slim:
PHPpublic function store(Request $request, CreateNewUser $createUser) { $user = $createUser($request->validated()); return response()->json($user, 201); }
This approach isn't just about aesthetics. By pulling logic into action classes, you gain several advantages:
CreateNewUser without needing to mock an entire Request object or hit your routing layer.CreateNewUser into your command's constructor.I’ve found that this Laravel service pattern (or action pattern) works best when you keep the classes focused. If your action class starts requiring five or six dependencies, it’s a sign that you might need to break it down further or use Laravel events and listeners to decouple the side effects, like sending emails.
I once spent about two days building a complex interface-based service layer, only to realize I was making the code harder to navigate. Don't fall into the trap of abstracting everything behind interfaces just because you think it’s "professional."
If you're just starting, keep it simple. If you find yourself needing to swap implementations, then introduce an interface. Until then, a plain PHP class is perfectly fine.
When you combine these actions with mastering Laravel traits for cleaner Eloquent models, you’ll find that your models stay lean, your controllers stay focused, and your business logic lives in a predictable, easy-to-find location.
Q: Should I use Service Classes or Action Classes? A: They are similar. I prefer Action classes for single-purpose operations (like "ProcessPayment") and Service classes for grouping related business logic (like "PaymentGatewayService"). Use what feels natural for your team.
Q: Where should I put these files?
A: I usually create an app/Actions or app/Services directory. Just make sure to register them in your PSR-4 autoloading in composer.json if you're using a custom namespace.
Q: Does this hurt performance? A: The overhead of an extra class instantiation is roughly 0.001ms in modern PHP. You won't notice it. The maintenance cost of a messy controller, however, is massive.

Refactoring is an ongoing process. You don't have to rewrite your entire application in an afternoon. Start by picking one bloated controller and moving its logic into an action class. You’ll feel the difference immediately when you go to write the tests.
I’m still experimenting with how to best handle complex transactions within these actions—sometimes I lean on the Transactional Outbox pattern in Laravel to keep things consistent—but the core principle remains: keep your code decoupled and keep your controllers thin.
Mastering Laravel Facades allows you to simplify complex service calls with clean, static-like syntax. Learn how to implement them while maintaining testability.