Architecting for Maintainability: Refactoring Laravel Controllers
Stop writing fat controllers. Learn how to identify controller bloat, extract logic into dedicated classes, and use dependency injection for cleaner code.
Welcome to the first step in our journey to build professional-grade Laravel applications. We are starting our project board application today, and the most important habit you can form is protecting your controllers from becoming "junk drawers."
In this course, we will focus on building a robust, testable, and scalable architecture. While Laravel interfaces and service contracts for cleaner architecture will be our goal later, we start here by mastering the basics of separation of concerns.
Identifying Controller Bloat
A controller's primary responsibility is to handle the HTTP request, coordinate with the application layer, and return a response. When you find yourself writing database queries, calculating business logic, or sending emails directly inside a controller method, you have "controller bloat."
Signs of bloat include:
- Methods exceeding 10 lines of code.
- Direct calls to
DB::table()or complex Eloquent queries inside the controller. - Business rules (e.g., "a user can only have 5 active projects") hardcoded in the method.
- Difficulty writing unit tests because the controller is tightly coupled to the database.
Refactoring Logic into Dedicated Classes
Let's look at a "bloated" example. Imagine we are creating a project in our project board.
PHPpublic function store(Request $request) { $request->validate(['name' => 'required']); #6A9955">// Logic bloat: calculating limits and setting defaults $count = Project::where('user_id', auth()->id())->count(); if ($count >= 5) { return response()->json(['error' => 'Limit reached'], 403); } $project = Project::create([ 'name' => $request->name, 'user_id' => auth()->id(), 'slug' => Str::slug($request->name), ]); return response()->json($project, 201); }
This logic belongs in a service class. By moving it, the controller becomes a thin "traffic cop." We will create a ProjectCreator class to handle this.
PHPnamespace App\Actions; use App\Models\Project; use Illuminate\Support\Str; class ProjectCreator { public function execute(array $data, int $userId): Project { if (Project::where('user_id', $userId)->count() >= 5) { throw new \Exception("Limit reached"); } return Project::create([ 'name' => $data['name'], 'user_id' => $userId, 'slug' => Str::slug($data['name']), ]); } }
Implementing Dependency Injection
Now, we inject this class into our controller. Laravel’s Service Container automatically resolves the class, making our controller clean and testable.
PHPpublic function store(Request $request, ProjectCreator $creator) { $request->validate(['name' => 'required']); try { $project = $creator->execute($request->all(), auth()->id()); return response()->json($project, 201); } catch (\Exception $e) { return response()->json(['error' => $e->getMessage()], 403); } }
This approach mirrors the principles discussed in Laravel Contextual Binding: Injecting Different Implementations Easily, where we treat components as interchangeable services rather than hardcoded dependencies.
Hands-on Exercise
- Create a directory
app/Actions. - Move the logic for adding a "Task" to a project (e.g., setting a default status) into a new class called
TaskCreator. - Inject
TaskCreatorinto yourTaskController. - Observe how much smaller your
storemethod becomes.
Common Pitfalls
- Over-engineering: Don't create a service class for a simple
User::find($id). Keep the architecture proportional to the complexity. - Circular Dependencies: If Service A needs Service B, and Service B needs Service A, you've likely missed a layer of abstraction.
- Hidden Dependencies: Avoid using the
app()helper inside your service classes. Always inject dependencies through the constructor to keep them visible and testable.
Recap
We've moved from writing procedural code in controllers to an architecture that separates concerns. By extracting logic into dedicated classes and using dependency injection, we ensure our project board remains maintainable as it grows. You now have a blueprint for "thin" controllers that will serve you throughout this course.
Up next: We'll dive deeper into structuring these classes by Implementing the Service Layer to standardize how our application handles complex business processes.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.