Domain Driven Design with Laravel 9
Modern web frameworks teach you to take one group of related concepts and split it across multiple places throughout your codebase. Laravel is a robust framework with a big community behind it. Usually it's standard structure is enough for most starting projects.
Building scalable applications, instead, requires a different approach. Have you ever heard from a client to work on controllers or review the models folder? Probably never - they ask you to work on invoicing, clients management or users. These concept groups are called domains.
Let's make a practical exercise applying Domain Driven Design. Our goal is to create a boilerplate to be used universally as base of any Laravel project. Take advantage of the framework power at the same time we meet complex business requirements.
Understanding of Domain Driven Design and some basic concepts:
We are going to use a fresh Laravel 9 installation for this guide, take a look on how to create your first Laravel project. To run Laravel locally a PHP setup is also required.
You also have the direct option to start the Laravel 9 project with domain driven design. The following command will create a new project in the laravel9-ddd folder and install the required dependencies:
composer create-project hibit-dev/laravel9-ddd
Keep in mind
We must keep in mind some important points planning the architecture of our software:
Clean-code design plays a key role in building highly scalable applications.
Follow unified business language that everyone in the company (not only developers) will understand and that will be used in our business/product development process.
Decoupling the application from the framework can be exhausting and pointless. We want to use the power of the framework having the code as much decoupled as possible.
Carefully choose your third-party services, otherwise, they might cause operational failure.
There are several ways in which the Laravel framework can be organized to serve as a template for large-scale projects. We will focus on the app (aka src) folder while keeping the framework features almost intact .
Initially, Laravel is structure looks as below:
With modified codebase structure, we are able to follow Domain Driven Design within our Laravel project which will support the future growth of our software. We also will be ready for the upcoming framework upgrades. We want it to be easy to upgrade to the next versions.
In first place, we should create a folder for each DDD layer:
Since this layer is where abstractions are made, the design of interfaces are included in the domain layer. It will also contain aggregates, value objects (VOs), data transfer objects (DTOs), domain events, entities, models, etc...
The only exception would be anything related to eloquent models. Eloquent makes very easy to interact with databases, tables and rows but the reality is that it's not a DDD model. It's an ambiguous definition of the concept of model with implementation of database connection. Does it mean that we can not use Eloquent? Yes we can, it can be used as repository implementation (infrastructure layer). We do have a significant advantage with this approach: we are no longer dependent on Laravel's method names and we can use some naming that reflects the language of the domain.
Actually we have nothing in domain layer so we will keep it empty.
Application layer provides the required base to use and manipulate the domain in a user-friendly way. It is where business process flows are handled, commands are executed and reactions to domain events are coded.
Actually we have nothing in application layer so we will keep it empty.
Infrastructure layer is responsible for communication with external websites, access to services such as database (persistence), messaging systems and email services.
We are going to treat Laravel as a third-party service for our application. So all the framework files are going to be grouped inside the infrastructure folder.
What does it imply:
Create app/Infrastructure/Laravel folder
Move the base controller app/Http/Controllers/Controller.php to app/Infrastructure/Laravel/Controller.php
Move HTTP Kernel app/Http/Kernel.php to app/Infrastructure/Laravel/Kernel/Http.php
Move Console Kernel app/Console/Kernel.php to app/Infrastructure/Laravel/Kernel/Console.php
Move app/Exceptions/Handler.php to app/Infrastructure/Laravel/Exceptions/Handler.php
Update bootstrap/app.php to point the correct Kernels and exception handler
Move app/Providers/* to app/Infrastructure/Laravel/Providers
Update config/app.php to point the correct Providers
Move app/Http/Middleware/* to app/Infrastructure/Laravel/Middleware
Update the HTTPKernel to point the correct middleware.
Note: make sure to update namespaces when moving files.
The final result look as following:
User Interface (UI)
User interface layer is the part where interaction with external world happens. The responsible of displaying information to the user and accepting new data. It could be implemented for web, console or any presentation technology.
Actually we have nothing in user interface layer so we will keep it empty.
One last thing that our architecture is lacking: the connection of concrete implementations with interfaces within our domain, e.g. repository interfaces.
For each module on the domain layer, we need a matching module in the infrastructure layer which takes responsibility for what the domain layer cannot afford to care about.
We recommend to use EventServiceProvider.php to bind domain events, commands and queries:
For other type of modules, AppServiceProvider.php is the best binding place:
The definition of the abstract interface and the concrete implementation in the service provider serves as class wiring configuration.
As a small bonus, we've included shared domain VOs for basic types.
That classes provide an abstraction and shared methods for the final VO definition. An example of usage:
<?php namespace App\\Domain\\PostValueObject;
class Message extends StringValueObject
Note: constructor, getters and additional shared methods can be included in the parent StringValueObject.
Note that so far nothing has changed in the way we use Laravel. We still have our Kernels, Providers, Exception Handlers, Rules, Mails and more inside the app folder.
Implementing Domain-Driven Design is always going to be a challenge no matter what framework we use, there is no unique way of defining things. Almost everything depends on the specific project you're working on and it probably makes sense to apply a different structure or architecture in other cases.
Domain Drive Design is a continuous process that must be carried out according to specific needs that can be adapted over time. Also it's a trade off: investing time on having a perfect structure or creating a starting base and improving with the time.
If you have an interest to explore further details about Domain Drive Design, we have an article available that includes a practical example. The article focuses on the user domain and utilizes the Laravel 9 structure created in this article:
We would be happy to receive your feedback and thoughts on it.
Official GitHub: https://github.com/hibit-dev/laravel9-ddd