🏠 Home 👤 About ⚡ Skills 💼 Portfolio 📦 Packages 📝 Blog ✉ Contact ✉ Contact Now
All Articles

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.

Junaid Ali 3 min read 0 views

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.

Tagged Laravel Architecture Clean Code PHP
Share this article

Want this built properly?

I take on a small number of projects at a time so each one gets real attention.

WhatsApp Teams LinkedIn Facebook GitHub