A fresh Laravel app groups files by their technical type: all controllers in one folder, all models in another, all jobs in a third. That works while the app is small. Grow it, and the layout starts to fight you. The fix is to structure a Laravel project by domain instead - a folder per business area, not per file type.
The problem with folders-by-type
Open app/Http/Controllers in a mature project and you'll find controllers for billing,
the catalog, users, shipping and reporting all sitting together. The same for app/Models,
app/Jobs, app/Events. To understand one feature you jump between five folders, and
those folders mix five unrelated concerns.
app/
Http/Controllers/
InvoiceController.php <- billing
ProductController.php <- catalog
ShipmentController.php <- shipping
Models/
Invoice.php <- billing
Product.php <- catalog
Shipment.php <- shipping
Nothing here tells you where "billing" begins and ends. This is the coupling problem from Chapter 1: type-based folders hide your real boundaries.
Group by domain instead
Turn the layout inside out. Make a folder per domain, and put everything that feature needs inside it - controller, model, service, job, whatever.
app/
Billing/
Invoice.php
InvoiceController.php
ChargeCustomer.php
Money.php
Catalog/
Product.php
ProductController.php
Shipping/
Shipment.php
ShipmentController.php
Now the folder names are the language of the business, not the language of the framework.
Each folder is a candidate boundary (Chapter 1) and, if you went further with
bounded contexts
in Chapter 3, often maps one-to-one onto them. A new developer reads app/Billing/ and
sees the whole feature in one place.
The PSR-4 note: namespaces follow folders
Laravel autoloads classes with PSR-4: the namespace mirrors the folder path under
app/, which maps to the App\ prefix in composer.json.
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
Because App\ already points at app/, moving Invoice.php into app/Billing/ just
means its namespace becomes App\Billing:
namespace App\Billing;
class Invoice
{
// ...
}
No extra Composer config is needed - the default App\ mapping covers every subfolder. If
you rename or move files, run composer dump-autoload to refresh the class map. One detail
that trips people up: Laravel 11 dropped the old controller-namespace prefix in the route
service provider, so routes already reference controllers by their full class name. Move the
controller, update the use line, and routing keeps working - only the namespace changed.
Common mistake: moving files but keeping the coupling
Reorganizing folders does nothing on its own if App\Billing still reaches into
App\Shipping's model and queries its tables directly. The layout is a place to put
boundaries, not a boundary by itself. The win comes when each domain folder talks to its
neighbours through a narrow, deliberate opening - a method or an interface - and hides the
rest. Folders by domain make the boundaries visible; you still have to keep them clean.
FAQ
How do I structure a Laravel project by domain?
Create one folder per business area under app/ (app/Billing, app/Catalog) and place
that feature's controller, model, and services inside it. The namespace follows the folder
thanks to PSR-4 (App\Billing\...), so no extra Composer config is needed beyond the
default App\ mapping.
Do I have to move controllers out of app/Http/Controllers?
No rule forces it. Some teams keep controllers in app/Http and only group the domain
logic by feature. Both work. The goal is that each domain's code lives together and its
boundary is easy to see - pick the split your team will actually keep tidy.
Won't this break routing or Artisan generators?
No. Routes reference controllers by their full class name, so a new namespace is all that
changes. Artisan's make:* commands accept a path (php artisan make:model Billing/Invoice),
and composer dump-autoload keeps autoloading in sync after you move files.