How I Structure a Laravel Project That Stays Maintainable After Year One
Most Laravel codebases do not rot because of bad code — they rot because nobody decided where things belong. Here is the folder and layer structure I use on every client project.
Every Laravel project starts clean. Six months in, the controllers are 400 lines long, business logic lives in three different places, and nobody wants to touch the payment flow. That is not a Laravel problem — it is a decision problem. Nobody decided where things belong.
Start With Boundaries, Not Folders
Before creating a single file I write down the boundaries of the system: what are the nouns, who owns them, and what talks to what. On a scheduling system that might be Course, Session, Instructor, Booking. Each of those gets a model, and each gets exactly one place where its rules live.
Controllers Stay Thin — Always
A controller does three things: validate input, call one service or action, return a response. That is it. The moment a controller starts branching on business rules, that logic moves into an action class.
public function store(StoreBookingRequest $request, CreateBooking $createBooking)
{
$booking = $createBooking->handle($request->validated());
return redirect()->route('bookings.show', $booking)
->with('success', 'Booking confirmed.');
}
One Action, One Job
I keep single-purpose action classes in app/Actions. CreateBooking, CancelBooking, SendBookingReminder. Each is a class with one public handle() method. They are trivial to test, trivial to queue, and trivial to reuse from a controller, a console command, or an API endpoint.
Form Requests Do the Validating
Inline $request->validate() is fine for a contact form. For anything with more than four fields it goes into a Form Request, where the rules are named, testable and reusable.
Query Scopes Over Repeated Where Clauses
If you write ->where('is_active', true)->orderBy('sort_order') more than twice, it becomes scopeActive(). Six months later when "active" also means "published_at is in the past", you change one line instead of forty.
Config and Settings, Never Magic Strings
Anything an admin might want to change goes into a settings table with a cached accessor. Anything a developer changes goes in config/. Magic strings scattered through Blade templates are how a site becomes un-editable.
The Test That Actually Matters
I do not chase 100% coverage. I write feature tests for the flows that lose money if they break: checkout, booking, authentication, and any webhook. That handful of tests catches the vast majority of real regressions.
The Result
The structure is boring on purpose. A new developer can open the project and answer "where does this go?" in under a minute. That single property is worth more than any clever abstraction.