← Thinking in Domains in Laravel Chapter 01

Thinking in Domains

If you have been building Laravel applications for a while, you have probably noticed that the default CRUD structure starts to fall apart once the project gets large enough. Controllers become fat, models handle too much, and business logic ends up scattered across random places.

This is a problem that many teams face once their project grows beyond a handful of models. The typical solution people reach for is Domain Driven Design (DDD), but full-blown DDD is often overkill for web applications. What works better is borrowing the best ideas from DDD, Hexagonal Architecture, and Event Sourcing, then applying them pragmatically within Laravel.

The core idea is deceptively simple: group code by what it represents in the real world, not by its technical role.

The problem with default Laravel structure

Laravel's default structure groups code by technical properties. Controllers live together, models live together, requests live together. This is fine when your project has 10 models and 15 controllers.

But think about this: has a product owner ever asked you to "refactor the models folder"? No. They ask you to work on course publishing, student enrollment, or the catalog.

When a project has 100 models, 300 actions, and 500 routes, the default structure means that any single business concept — like enrollment — is spread across dozens of directories. You need to look in the controllers directory, the models directory, the requests directory, the jobs directory, and more. The cognitive load becomes enormous.

What is a domain?

A domain is a group of related business concepts. You could also call them "modules" or "services". In an online education platform, your domains might be:

  • Students
  • Enrollments
  • Courses
  • Catalog

Each domain groups everything related to that business concept together:

src/Domain/Courses/
├── Actions
├── QueryBuilders
├── Collections
├── Data
├── Events
├── Exceptions
├── Listeners
├── Models
├── Rules
└── States
src/Domain/Students/
├── Actions
├── Models
├── Events
└── Rules

Notice that each domain can have a different internal structure. The Courses domain might need states and query builders, while the Students domain might be simpler. There is no requirement for uniformity.

Domains vs applications

There is a key distinction between domain code and application code.

Domain code is your business logic. It contains models, DTOs, actions, validation rules, events, and states. It represents what your application does.

Application code is how that logic gets exposed to users. It contains controllers, middleware, form requests, resources, and view models. It represents how users interact with your business logic.

A single project can have multiple applications consuming the same domain:

src/App/Dashboard/    # Instructor/admin dashboard (Blade/Inertia)
├── Controllers
├── Middlewares
├── Requests
├── Resources
└── ViewModels

src/App/Api/          # REST API
├── Controllers
├── Middlewares
├── Requests
└── Resources

src/App/Console/      # Artisan commands
└── Commands

All three applications can use the same Domain\Courses code. The dashboard might render a course editor form, the API might return course JSON for a mobile app, and a console command might publish scheduled courses. The business logic is identical; only the delivery mechanism differs.

This separation is powerful. When you need to change how course enrollment works, you change the domain code once. All applications automatically get the update.

Look at the folder paths above — there is no single \App root namespace like a default Laravel project. That is intentional. When your project has multiple application layers (a dashboard, an API, a CLI), cramming them all under \App stops making sense. So we give each layer its own root: \App for application code, \Domain for business logic, and \Support for shared utilities.

Setting up the folder structure

To make this work with Laravel, you update composer.json to add two new root namespaces:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/App/",
            "Domain\\": "src/Domain/",
            "Support\\": "src/Support/"
        }
    }
}

The Support namespace is a home for generic helpers that do not belong to any specific domain. Think of it as code that could just as well be a standalone package: a MarkdownRenderer, a SlugGenerator, a FileUploader.

Updating composer.json handles class autoloading, but Laravel itself still expects your application code to live in the default app/ directory. Internally, Laravel uses the app path to discover commands, resolve providers, and locate other framework-convention files. If you move your application layer to src/App/, you need to tell Laravel about it.

Open your bootstrap/app.php file and call useAppPath on the application instance before returning it:

$app = Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

$app->useAppPath($app->basePath('src/App'));

return $app;

The useAppPath method redirects every internal call to app_path() to your new directory. Without this, Artisan's make: commands would still generate files inside app/, and any framework feature that relies on the app path would look in the wrong place.

If you want to keep Laravel's default app/ directory instead of src/App/, you can skip this step entirely. The important thing is the mindset of separating domain from application, not the exact folder names.

Domains evolve

One of the most important things to understand is that domain boundaries are not permanent. You might start with a Courses domain and realize six months later that it has grown too large. Maybe course authoring and course delivery are complex enough to be their own separate domains.

That is fine. Refactoring domain boundaries is cheap when each domain has minimal dependencies on others. You are not locked into your initial structure.

Do not overthink the perfect domain structure before writing code. Start with what makes sense now, and refine it as your understanding of the business grows. The architecture supports change — that is the whole point.

When to use this architecture

This approach is not for every project. A simple CRUD app with 5 models does not need separate domain and application layers. The overhead would outweigh the benefits.

But when your project has:

  • Dozens of models with complex relationships
  • Multiple developers working simultaneously
  • Years of expected maintenance ahead
  • Complex business rules that go beyond simple data persistence

Then grouping by business concept instead of technical role will save you significant time and headaches. The rest of this book will show you exactly how.

If your project is not yet large enough for domain-oriented architecture, Clean Code in Laravel covers how to write clean, maintainable code within Laravel's default structure. Many of its principles — naming, dependency injection, thin controllers — apply regardless of how you organize your folders.

Summary

  • Laravel's default structure groups code by technical role (controllers, models, requests). This works for small projects but breaks down as the codebase grows, because a single business concept ends up scattered across dozens of directories.
  • A domain is a group of related business concepts. Group code by what it represents in the real world — courses, students, enrollments — not by its technical type.
  • Domain code contains your business logic (models, DTOs, actions, events, states). Application code is how that logic gets exposed to users (controllers, middleware, requests, resources). Multiple applications can consume the same domain.
  • Set up the structure by adding App, Domain, and Support namespaces to composer.json, and call useAppPath() in bootstrap/app.php to redirect Laravel's internal app path.
  • Domain boundaries are not permanent. Start with what makes sense now and refine as your understanding of the business grows. Refactoring is cheap when domains have minimal dependencies on each other.
  • This architecture is best suited for projects with dozens of models, multiple developers, years of expected maintenance, and complex business rules beyond simple CRUD.

References

Share