DEV Community

Cover image for Pragmatic Domain-Driven Design in Laravel, with Laravel Boost
Matteo Barbero
Matteo Barbero

Posted on

Pragmatic Domain-Driven Design in Laravel, with Laravel Boost

Laravel Boost is widely used to give coding agents context about Laravel projects. It can provide guidelines and skills that shape how agents create and change files. That made me ask a question: could Boost also be a place to guide an application toward Domain-Driven Design?

I wanted to try that without asking people to remove Laravel from their domain code. Eloquent models, policies, events, queues, service providers, and the usual Laravel entry points are useful parts of an application. DDD, as I wanted to apply it, should help decide where behaviour belongs, not require replacing those tools with a framework of our own.

That idea led me to build Laravel Boost DDD.

The boundaries matter more than the folder names

The package gives Boost a set of guidelines and task-specific skills for working with an adopted DDD structure.To be sure that the agent follows the architecture I had in mind, the package also publishes Pest architecture tests to check some of the dependency boundaries.

The basic structure is:

  • App\Domain\<Capability> for business behaviour and invariants.
  • App\Application\<Capability> for use cases and orchestration.
  • App\Infrastructure for external integrations and adapters.
  • Laravel’s usual directories for controllers, commands, jobs, listeners, policies, providers, migrations, and factories.

The word “capability” here does not mean every folder is a separate bounded context. Orders and Billing might be separate capabilities in the same context, and their models may collaborate directly.

Consider cancelling an order. The order model can own the rule about which states allow cancellation:

public function cancel(): void
{
    if ($this->status !== 'pending') {
        throw new OrderCannotBeCancelled();
    }

    $this->status = 'cancelled';
}
Enter fullscreen mode Exit fullscreen mode

An application Action coordinates the use case: load the order, authorise the actor through a Laravel policy, invoke cancel(), and save the model. If the operation also writes an audit record, the Action can put both writes in a database transaction.

The model is still an Eloquent model. The policy is still a Laravel policy. The Action uses Laravel’s authorisation and database facilities. The boundary is about responsibility: the model decides whether its state transition is valid, while the Action coordinates the use case.

Use abstractions when the code needs them

A DDD structure can encourage people to add repositories, DTOs, domain services, factories, and adapters everywhere. Those patterns are useful when they solve a real problem, but they can also make ordinary work harder to follow.

In this approach, direct Eloquent persistence is the default. A scalar order ID does not need a Data object wrapped around it. A simple authorised read can stay in a controller. A repository makes sense when direct Eloquent is insufficient, not because every model must be hidden behind one.

Contracts belong at actual dependency boundaries. If an Action needs a payment gateway, the application defines the contract it needs and Infrastructure provides the implementation. That keeps the application independent of a specific SDK while leaving Laravel’s container available to connect the pieces.

The goal is a structure with meaningful boundaries and as little extra machinery as the use case allows.

Adoption can happen one feature at a time

Installing the package does not move existing application code. New capabilities can follow the structure, while legacy code stays where it is until migration is part of the work.

That matters because introducing boundaries and reorganising an application are separate tasks. If I am fixing a bug in an existing controller, that does not automatically mean I should move the controller’s whole feature into new namespaces. If I am building a new capability, I can start with the structure and apply the boundaries there.

The architecture tests follow the same rule. They check namespaces such as App\Domain and App\Application when those directories exist. An application that has not adopted those layers gets skipped checks. Once code enters those namespaces, it is covered by the rules, while legacy code outside them remains untouched.

What Boost contributes

Boost provides the place where an agent can receive these conventions while working in a Laravel project. Laravel Boost DDD adds guidelines for the overall structure, skills for specific work such as creating an Action or testing a domain rule, and architecture tests for a small set of static constraints.

The tests can check, for example, that Domain code does not depend on Application or Infrastructure, and that Actions expose a handle() method. They cannot establish that authorisation is correct, that a transaction covers the right writes, or that an event is dispatched at the right time. Those behaviours still need ordinary tests and review.

The package is an attempt to make one particular balance explicit: keep Laravel as the application framework, use DDD boundaries where they clarify responsibility, and adopt the structure incrementally. I’m interested in how other Laravel teams draw that line, especially where they find that an abstraction starts paying for itself.

Top comments (0)