0% found this document useful (0 votes)
375 views49 pages

Domain-Driven Design With Laravel Sample Chapter

This document provides an overview of Domain-Driven Design (DDD) and discusses some of its key concepts. DDD aims to align software design with the business domain by structuring code in a logical and expressive way using domains, proper naming conventions, and classes/objects that clearly convey their purpose. The document discusses strategic design and technical design aspects of DDD and provides examples of value objects, data transfer objects, repositories, and other concepts.

Uploaded by

Rafael Lessa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
375 views49 pages

Domain-Driven Design With Laravel Sample Chapter

This document provides an overview of Domain-Driven Design (DDD) and discusses some of its key concepts. DDD aims to align software design with the business domain by structuring code in a logical and expressive way using domains, proper naming conventions, and classes/objects that clearly convey their purpose. The document discusses strategic design and technical design aspects of DDD and provides examples of value objects, data transfer objects, repositories, and other concepts.

Uploaded by

Rafael Lessa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 49

DOMAIN-DRIVEN

DESIGN WITH
LARAVEL
The only design approach you need

MARTIN JOO
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

This is a sample chapter from the original book that you can find here.

1 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Basic Concepts
Domain-Driven Design
First of all, we have to answer the most obvious question: what is Domain-Driven Design.

DDD is a software development approach that tries to bring the business language and the code as close
together as possible. This is the most critical attribute of this approach. But for some reason, DDD is one of
the most misunderstood and overcomplicated topics in the developer community, so I'll try to make it easy
to understand.

DDD teaches us two main things:

Strategic Design
Technical Design

In my honest opinion, strategic design is way more important than the technical aspects. It's hard to
summarize it in one cool sentence, but you will see what I mean in the rest of the book. For now, these are
the essential pillars:

Domains and namespaces. Later, I'll talk about what a domain is, but DDD teaches us to structure our
code very expressive and logical.
Choosing the proper names. For example, if the business refers to the users as "customers" or
"employees, " you should rename your User class to follow that convention.
The classes and objects should express the intention behind them. In the most simple Laravel
application, you have models and controllers. What do you think when you see a project with 50
models and 50 controllers? You can see the application's primary domain, but you have to dig deeper if
you want to have a good understanding of the features, right? Now, what about 300 models and 500
controllers? You have no chance to reason about this kind of application.

In most projects, developers prefer technical terms over business concepts. That's natural. After all, we're
technical people. But I have a question: are those technical terms significant?

Let me show you an example. This is a snippet from one of my applications I wrote on Oct 28, 2016, after
finishing the Design Patterns book. Take a look at it (it's not Laravel):

2 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class Search_View_Container_Factory_Project
{
/**
* @var Search_View_Container_Relation_Project
!"
private static $_relationContainer;

/**
* @param array $data
* @return Search_View_Container_Project
!"
public static function createContainer(array $data)
{
if ($data['current'] !# 'complex') {
return self!$createComplex($data);
} else {
return self!$createSimple($data);
}
}

/**
* @param array $data
* @return Search_View_Container_Project
!"
private static function createSimple(array $data)
{
$container = new Search_View_Container_Project('simple');
$container!%setSearchTerm(Arr!$get($data, 'search_term'));

$relationContainer = new Search_View_Container_Relation_Project();


$industryModel = new Model_Industry();
$industries = $industryModel!%getAll();

foreach ($industries as $industry) {


$industryItem = new Search_View_Container_Relation_Item(
$industry,

3 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Search_View_Container_Relation_Item!$TYPE_INDUSTRY,
false
);

$relationContainer!%addItem($industryItem,
Search_View_Container_Relation_Item!$TYPE_INDUSTRY);
}

$container!%setRelationContainer($relationContainer);
return $container;
}

/**
* @param array $models
* @param int $type
!"
private static function addItems(array $models, $type)
{
foreach ($models as $model) {
$item = new Search_View_Container_Relation_Item($model, $type, true);
self!$$_relationContainer!%addItem($item, $type);
}
}
}

Today it's February 2, 2022. What do you think? After six years, do I have any clue what the heck is a
Search_View_Container_Relation_Item? No, I have no idea what it is. I only know one thing for sure: it does
not help me. This project is about freelancers and projects. This class does something with searching
projects (I guess), but it does not reveal that intention. Did you ever hear a product manager saying: wow, we
got so much positive feedback on the Search View Container Factory Project feature?

Maybe if I take a step back and look at the file structure, I have a better idea.

4 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Nope, still have no idea. Here's my point:

5 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Technical terms and overused patterns suck when it comes to high-level business applications.

So strategic design is all about not building projects like this one. And technical design gives you some
valuable tools to achieve that. In the following pages, we'll talk about these concepts:

Value Objects
Data Transfer Objects
Repositories
Custom Query Builders
Services
Actions
View Models
CQRS
States and Transitions
Domains and Applications

6 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Working With Data


Working with data is one of the most critical aspects of every business application. Unfortunately, PHP is not
so good when it comes to this. In my opinion, one of the best and worst features of PHP is arrays. Especially
associative arrays. The problems are:

No type-hints
Undocumented structure
No restrictions. You can put product models, product IDs, and product arrays under the same key.

Associative arrays are big unstructured blobs of data. Don't get me wrong; they can be helpful but very
annoying at the same time. Initially, PHP arrays tried to solve every problem: queues, stacks, lists, hash
maps, and trees. Everything. But with its weak type system, it's tough to maintain this kind of data structure.

If you think about it, data plays a huge role in any business application:

The request comes in. It contains the incoming data.


The business layer processes this data.
The database layer inserts this data into the DB.
The response comes out. It includes the outgoing data.

So you have to work with data in every single layer of your application. Fortunately, Laravel and DDD give us
some very clever concepts.

7 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Value Objects
Value Object is an elementary class that contains mainly (but not only) scalar data. So it's a wrapper class
that holds together related information. Let's see an example:

class Percent
{
public readonly ?float $value;
public readonly string $formatted;

public function !&construct(float $value)


{
$this!%value = $value;

if ($value !!' null) {


$this!%formatted = '';
} else {
$this!%formatted = number_format($value * 100, 2) . '%';
}
}

public static function from(?float $value): self


{
return new self($value);
}
}

This class represents a percentage value. This simple class gives you three advantages:

It encapsulates the logic that handles null values and represents them as percentages.
You always have two decimal places (by default) in your percentages.
Better types.

An important note: business logic or calculation is not part of a value object. The only exception I make is
basic formatting.

By better types, I mean methods like this:

8 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

private function averageClickRate(int $total): Percent


{
return Percent!$from(
SentMail!$whereClicked()!%count() / $total
);
}

You take a float value and make it a first-class citizen using a Percent value object. You don't have to worry
about anymore if a method in your app returns a formatted string or a float number. Every percentage
value can be expressed as a Percent object from now on. So you know, it contains the float number and the
formatted string value.

The original definition of a value object states two more things:

It's immutable. You have no setters and only read-only properties.


It does not contain an ID or any other property related to the identification. Two value objects are
equal only when the values are the same.

What else can be expressed as a value object? Almost anything, to name a few examples:

Addresses. In an e-commerce application where you have to deal with shipping, it can be beneficial to
use objects instead of strings. You can express each part of an address as a property:

City
ZIP code
Line 1
Line 2
Numbers. Any financial application can benefit from using value objects when calculating metrics or
comparing numbers. You can express some very high-level concepts, for example, Margin.

Top Line (such as revenue)


Bottom Line (such as net profit)
Margin (as a Percent, of course)
Email addresses

Or other application-specific concepts

Let's take a closer look at the Margin example:

9 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class Margin
{
public function !&construct(
public readonly float $topLine,
public readonly float $bottomLine,
public readonly float $margin,
){}
}

Suppose you've worked with financial applications that deal with publicly traded companies. You know that
a number like revenue is given in millions (or billions in some cases, for example, the market cap). So when
you query Apple's revenue (which is 378 billion at the time of writing) from a finance API, you don't get
378,323,000,000 but 378,323, so we can express it in the code as well:

class Margin
{
public function !&construct(
public readonly Millions $topLine,
public readonly Millions $bottomLine,
public readonly Percent $margin,
){}
}

And we can use the Margin class like this:

10 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class MetricsService
{
public function profitMargin(IncomeStatement $incomeStatement): Margin
{
return new Margin(
topLine: $incomeStatement!%revenue,
bottomLine: $incomeStatement!%net_profit,
margin: new Percent(
$incomeStatement!%net_profit!%value / $incomeStatement!%revenue-
>value
),
);
}
}

In this example, I assume that revenue and netProfit are instances of Millions . But isn't
IncomeStatement an Eloquent model? Glad you asked. It is. And we can write a custom cast to convert
floats to Millions :

class MillionsCast implements CastsAttributes


{
/**
* @param float $value
!"
public function get($model, $key, $value, $attributes)
{
return new Millions($value);
}

/**
* @param Millions $millions
!"
public function set($model, $key, $millions, $attributes) {
return [
$key !( $millions!%value,
];
}

11 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

It can be used in an Eloquent model, and here's how it works:

When you're accessing an attribute on the model, the get method will be called. So
$incomeStatement->revenue will return an instance of Millions .
When you're setting an attribute on the model, the set method will be called. So $incomeStatement-
>revenue = new Millions(1000) will insert the value property (1000) from the Millions instance.

The last part is to use the cast in the model:

protected $casts = [
'revenue' !( MillionsCast!$class,
'net_profit' !( MillionsCast!$class,
];

So, in a nutshell, this is how you use a value object. To summarize it:

By using value objects, you can make objects from cohesive scalar data

The main benefits:

It makes your code more high-level.


It clarifies things and helps to avoid confusion. For example, now you know exactly that Millions
contains a number stored in millions.
It helps you deal with nullable values. You don't have to write ?float $revenue anymore. You can
write Millions $revenue .

In the introduction, I wrote that data is a crucial part of any application. I gave you this list:

The request comes in. It contains the incoming data.


The business layer processes this data.
The database layer inserts this data into the DB.
The response comes out. It includes the outgoing data.

As you can see in the cast and the other examples, a value object is mainly used inside (but not exclusively!)
our application. In the next chapter, we'll discuss what happens at the boundaries (requests and responses).

12 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Data Transfer Objects


The following important concept is data transfer object, or DTO for short. This is also a simple concept: it's a
class that holds data. This data is then transferred between components. What are these components?

Your application as a whole


Classes inside your application

Let's take a look at a straightforward example:

class CourseController extends Controller


{
public function store(Request $request): Course
{
$course = Course!$create($request!%course);

foreach ($request!%lessons as $lessson) {


!) $lesson is an array
$course!%lessons()!%create($lesson);
}

foreach ($request!%student_ids as $studentId) {


$course!%students()!%attach($studentId);
}

return $course;
}
}

This is an oversimplified example, of course. I'm working on an e-learning system, and you can believe me,
the request for creating a new course is overwhelming.
After time this action will become more complicated, and you want to refactor it. Let's move this into a
service (later, we'll talk about services in more detail. For now, it's a class that implements some business
logic):

13 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class CourseService
{
public function create(array $data): Course
{
$course = Course!$create($data);
$this!%createLessons($course, $data['lessons']);
$this!%addStudents($course, $data['student_ids']);

return $course;
}

public function createLessons(Course $course, array $lessons): void


{
foreach ($lessons as $lessson) {
!) $lesson is an array
$course!%lessons()!%create($lesson);
}
}

public function addStudents(Course $course, array $studentIds): void


{
foreach ($studentIds as $studentId) {
$course!%students()!%attach($studentId);
}
}
}

It looks okay, but can you spot the problems?

Arguments like these: array $data or array $lessons


Lines like this: $data['lessons']

My biggest problem is that I don't want to maintain and debug massive associative arrays five years
later.

The above example is very basic. Now, please imagine your favorite legacy project, where you have to work
with methods like this:

14 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

public function createProduct(array $data)


{
!) Insert 673 lines of code here

/**
* You have to reverse-engineer this whole shit-show
* just to get an idea about the shape of $data, right?
!"
}

DTOs can solve this problem by structuring your unstructured data. The same CourseService with DTOs:

class CourseService
{
public function create(CourseData $data): Course
{
$course = Course!$create($data!%all());
$this!%createLessons($course, $data!%lessons);
$this!%addStudents($course, $data!%student_ids);

return $course;
}

/**
* @param Collection<LessonData> $lessons
!"
public function createLessons(Course $course, Collection $lessons): void
{
foreach ($lessons as $lessson) {
!) $lesson is an instance of LessonData
$course!%lessons()!%create($lesson);
}
}

public function addStudents(Course $course, Collection $studentIds): void


{

15 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

foreach ($studentIds as $studentId) {


$course!%students()!%attach($studentId);
}
}
}

Now, instead of arrays, we have objects like CourseData and LessonData . Let's take a look inside
CourseData :

class CourseData
{
public function !&construct(
public readonly int ?$id,
public readonly string $title,
public readonly string $description,
/** @var Collection<LessonData> !"
public readonly Collection $lessons,
/** @var Collection<int> !"
public readonly Collection $student_ids,
) {}

public static function fromArray(array $data): self


{
$lessons = collect($data['lessons'])
!%map(fn (array $lesson) !( LessonData!$fromArray($lesson));

return new self(


Arr!$get($data, 'id'),
$data['title'],
$data['description'],
$lessons,
collect($data['student_ids']),
);
}
}

16 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

The only place you have to deal with arrays with this approach is the DTO itself. Only the factory function
will know anything about the ugly $data array. Every layer of your application will use a structured, type-
hinted object.

As you can this class does not interact with Request or any other class that is environment-dependent so
that you can use DTOs anywhere, including:

Controllers
Console Commands
Services or Actions (covered later in the book)
Models or Query Builders (covered later in the book)

And here's how you can use it from the CourseController :

class CourseController extends Controller


{
public function store(
Request $request,
CourseService $courseService
): Course {
return $courseService!%create(
CourseData!$fromArray($request!%all())
);
}
}

I think that's a much better approach, especially in larger projects. But now, we have another problem. Just
imagine how many classes we need to create to store a course:

CreateCourseRequest
CourseData
CourseResource
LessonData
LessonResource
A few value objects here and there

And in this example, we have only two models! What if the domain model of this feature is much more
difficult? You can quickly end up with 10-15 classes to implement the CRUD functionality for courses. It's not
the end of the world, but it can be very frustrating. Fortunately, we have an elegant solution. But first, let's
summarize what a DTO is:

It's an object that holds and transfers the data of a model.


It can be used inside your application between components. Like in the example, when we created a
DTO in the controller from a request and passed it to a service.
But it can also be used outside of your application. So instead of having a request, a resource, and a
DTO for the course, why not just have one DTO to rule them all?

17 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Enter the laravel-data package by Spatie. You can use one DTO to act as a:

Request (with validation rules)


Resource
And a simple DTO

Important note: If you want to use DTOs you don't need to go with laravel-data. You can write pure PHP
objects, and you'll do just fine. But I find this package so helpful; I cannot imagine a large project without it.

This is what a laravel-data DTO looks like:

class SubscriberData extends Data


{
public function !&construct(
public readonly ?int $id,
public readonly string $email,
public readonly string $first_name,
public readonly ?string $last_name,
/** @var DataCollection<TagData> !"
public readonly null|Lazy|DataCollection $tags,
public readonly null|Lazy|FormData $form,
) {}
}

The basics are very similar to a pure PHP DTO, but we have this Lazy thing. I will talk about it later, but it's
very similar to the whenLoaded method used in Laravel resources (it helps us avoid N+1 query problems).
So we have a subscriber with a nested TagData collection and a nested FormData property.

This package can create a DTO from a request automatically. Since it can be used as a request, we can
define validation rules:

18 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

public static function rules(): array


{
return [
'email' !( [
'required',
'email',
Rule!$unique('subscribers', 'email')!%ignore(request('subscriber')),
],
'first_name' !( ['required', 'string'],
'last_name' !( ['nullable', 'sometimes', 'string'],
'tags' !( ['nullable', 'sometimes', 'array'],
'form_id' !( ['nullable', 'sometimes', 'exists:forms,id'],
];
}

We can also specify how we want the package to create a SubscriberData from an HTTP request:

public static function fromRequest(Request $request): self


{
return self!$from([
!!*$request!%all(),
'tags' !( TagData!$collection(
Tag!$whereIn('id', $request!%collect('tags'))!%get()
),
'form' !( FormData!$from(Form!$find($request!%form_id)),
]);
}

You can see that I run some DB queries to get the tags and the form. So instead of IDs (that come from the
request), I have models which can map to TagData or FormData . Later I'll explain these things in more
detail.

And look at the controller:

19 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class CreateSubscriberController extends Controller


{
public function !&invoke(SubscriberData $data): SubscriberData
{
return SubscriberData!$from(
CreateSubscriberAction!$execute($data)
);
}
}

You can inject any Data class, and transformation from the request will happen automatically! And as you
can see, a Data object can be returned from a controller action and will be cast to JSON (including the
nested properties).

In my opinion, it's a fantastic tool to have, so I'll use it heavily in the demo application later.

One more question to end up this chapter: what's the difference between value objects and DTOs?

A DTO has an ID because it represents a model.


A value object never has an ID. It represents a value, not an entity.

That's the main difference. However, in the Laravel community, I often see people mixing up the two
concepts. You can even see people writing only value objects, and they use it as a DTO and as a VO.

And you know what? It's perfectly okay, in my opinion.

Of course, these rules are essential, but I don't see them as strict rules that you have to follow if you want
DDD. I rather see them as guidelines. For example, later in the demo application, I will use a DTO instead of
a value object because it's much more convenient in the given situation.

20 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Repositories
Now that we've learned about data, we can move on to the database. Where to put your queries? How to
structure your code? We've all asked these questions, and here's the answer: no one knows precisely. It's
different for every project and every team.

One way to organize your queries is by using repositories. It's a controversial topic in the Laravel and PHP
community. I won't be using them in the demo application, but it's a DDD concept and, to be honest, not a
bad one. At the end of this chapter, I'll show you the Laravel equivalent of the repository pattern.

This is a repository class:

class ProductRepository
{
public function create(ProductData $data): Product
{
Product!$create($data!%all());
!) Other database related code
}

public function search(


PriceRange $priceRange,
string $searchTerm
): Collection {
return Product!$query()
!%whereBetween('price', [$priceRange!%low, $priceRange!%high])
!%where('name', 'like', "%{$searchTerm}%")
!%limit(10)
!%get();
}
}

It's a class for your database queries. That's it. So instead of writing your queries inside your controllers or
models, you move them into a repository. Usually, each model has a repository, but that's not necessarily
true every time.

You can use it like this:

21 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class ProductSearchController extends Controller


{
public function index(Request $request, ProductRepository $products)
{
return $products!%search(
PriceRange!$from($request),
$request!%search_term
);
}
}

So it seems like a good concept; why are they being hated? There are several problems:

Usually, each (or the vast majority of) model has a repository, such as the ProductRepository in this
example. So you're literally just moving a query from one place to another, from the model to the
repository. After six months of development, the ProductRepository will become a 5000 lines
monster. Just like the Product model would become a 5000 lines monster.
By definition, when using repository classes, every database query should end up in one of them. So
you need to move single-use queries as well. You'll quickly end up with a class containing methods
used one time in the entire application.
If you don't put every query inside a repository, then why do you even use repositories in the first
place? In this case, you'll end up with an inconsistent structure. Some queries are in controllers or
models; others live in a repository. In my experience, it's not an optimal solution.
This is a personal one: I don't feel that $products->search() or $this->products->getById($id)
fits well into Laravel.

However, repositories have some good attributes as well:

You don't need to have one repository for every model. Let's imagine you have a big enterprise project
with 200+ tables. One "module" or feature set is a very basic issue tracker. This feature set requires
only six tables and 500 lines of database-related code. You don't have to spread these 500 lines of code
in 6 repositories (as you would do with models). You can write only one class called
IssueTrackerRepository . You cannot do that with models. I think it can be helpful in some
situations.

To be honest, that's the only benefit I can think of. If you're already familiar with DDD, you probably heard
something like this: The repository pattern abstracts the data store and enables you to replace your database
without changing your business code. That's true. However, in the last decade, I've faced a lot of strange
feature requests, but two of them never came up:

Nobody ever asked me to change the programming language of an existing project.


Nobody ever asked me to change the database engine under a running project.

So I'm not authentic to talk about it as an advantage. That being said, I don't think repositories are helpful in
most situations.

22 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

In fact, Laravel has a better solution: custom query builders.

23 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Custom Query Builders


First, let's discuss what a query builder is. When you write something like that:

$query = Product!$where('name', 'Domain-Driven Design with Laravel');

You're interacting with the Illuminate\Database\Eloquent\Builder class. It contains your favorite


Eloquent methods such as where , firstWhere , latest , etc.

But in Laravel, we don't write code like this:

Builder!$where('description', 'LIKE', "%{$searchTerm}%");

We call the where method on the Model classes. So there has to be some connection between Model and
Builder . This connection is the newEloquentBuilder in the base Model class:

/**
* Create a new Eloquent query builder for the model.
*
* @param \Illuminate\Database\Query\Builder $query
* @return \Illuminate\Database\Eloquent\Builder|static
!"
public function newEloquentBuilder($query)
{
return new Builder($query);
}

It returns a new Builder instance, and every time you interact with your model, this instance will be used.
So when you call Product::where the newEloquentBuilder method will be called, it returns a new
Builder .

We can extend this base Builder class and can create a custom query builder:

24 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class MailBuilder extends Builder


{
public function whereOpened(): self
{
return $this!%whereNotNull('opened_at');
}
}

This is a custom builder for the Mail model. The whereOpened method can be used as a scope. In fact,
model scopes are just syntactic sugar. So this method can be used like this:

Mail!$whereOpened()!%get();

In the builder we need to return self to be able to chain Eloquent methods:

Mail!$whereOpened()!%where('title', 'First Mail')!%get();

However, you don't have to write scope-like methods. In a query builder, you can do (almost) anything:

class DividendPayoutBuilder extends Builder


{
public function sumByDate(DateFilter $dates, User $user): float
{
return $this!%whereBelongsTo($user)
!%wherePayedBetween($dates)
!%sum('amount');
}
}

$dividendThisMonth = DividendPayout!$sumByDate(DateFiler!$thisMonth());

This method cannot be chained because it returns a float. The last piece of the puzzle is to instruct Laravel
that we want to use our query builder. This can be done by overwriting the newEloquentBuilder method in
your model:

25 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class Mail extends Model


{
public function newEloquentBuilder($query): MailBuilder
{
return new MailBuilder($query);
}
}

And that's it! A query builder is the Laravel equivalent of a repository if you think about it. But in my
opinion, it feels much more like Laravel. To sum it up:

I prefer query builders over repositories.


I usually write every scope in builders.
And also often used methods.
But I don't put every query in builders.

This way, models remain very simple and thin. Later I will discuss where to put the single-use queries.

26 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Services
A service class means a lot of different things for many developers. But in general: it's a class that holds
some business logic. For example:

class TodoNotificationService
{
public function sendDueTodayNotifications(): void
{
Todo!$whereDueToday()
!%get()
!%each(fn (Todo $todo) !( $todo!%user!%notify(
new DueTodayNotification($todo)
));
}
}

Services are often used to wrap some external services, such as Github, Twilio, and others. But they can be
more low-level, something like this:

class TodoService
{
public function create(TodoData $data): Todo
{
$todo = Todo!$create($data!%all());
!)!!*
}
}

So services can be used as repositories? In theory, no, but in practice, yes. And a lot of developers use
services instead of repositories.
Can services be used together with repositories? Yes, but I don't recommend this approach.

When you have repositories and services as well in a project, this is the main idea:

Each model has a repository.


Repositories contain database queries.
When needed, you create a service to a model (or a set of models, just as we discussed in the
repository chapter). For example, the Todo class has more complex notification logic, so you might add
a TodoNotificationService .

27 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

This approach is fine and can work, but here's the main problem:

You end up with inconsistent classes. When you're working with the Todo model, you have a
repository and a service. But when you're working on a Project related feature, you only have a
repository because the project doesn't require a service class. And in the case of Todo , you won't be
able to tell quickly if a method lives in a service or a repository.
So, in general, your features are spread across services and repositories and a bit harder to reason
about.

This isn't very objective, so it may work very well for you! As for me, I stick with actions.

28 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Actions
What if we can combine repositories and services? We probably will have a big mess, like a fat controller
that does everything, right? Yeah, it's true. But let's forget about technical responsibilities for a minute. In
the first chapter, I wrote that technical code sucks in a business application. I think that's why I dislike
repositories and services being used alongside each other.

But what if we specify the responsibility based on features instead of technical stuff? Now, we have a clean,
reusable action class:

class CreateTodoAction
{
public function execute(TodoData $data): Todo
{
$todo = Todo!$create($data!%all());

if (!$todo!%creator!%is($todo!%assignee)) {
$todo!%assignee!%notify(new TodoAssignedNotification($todo));
}

$todo!%watchers!%each(fn (User $watcher) !(


$watcher!%notify(new TodoCreatedNotification($todo))
);

return $todo;
}
}

Action classes have some significant advantages:

Single responsibility. Each action takes care of precisely one thing, like creating a todo.
Self-contained. An action completes a task from beginning to end.
Nesting. An action can call any other action. If creating a todo is a complex task, maybe it's a good idea
to extract the notification logic.
Queueable. Not by default, but with the help of Spatie's laravel-queueable-action package.
But here's the biggest one: your actions describe your user stories. So they bring your code closer to
the business language. Just take a look at this:

29 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

If a new developer joins your team, he/she will know exactly what your application can do with subscribers:

Create a subscriber
Filter subscribers
Import subscribers

There's no need to dig deep into controllers, models, or services. It's clean and straightforward. In my
opinion, it's a huge win. As a bonus, actions are widely used in the Laravel community. By the way, this
example comes from the sample application we'll build throughout the book. As you can see, it's going to be
a lot of action.

There's one more detail we can discuss. There are three ways to write functions in an action.

Non-static execute method

This is the most obvious choice, and you cannot go wrong with it. I used this in the previous examples.

30 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Invokable class

In this case, you can write a class such as this:

class CreateTodoAction
{
public function !&invoke(TodoData $data): Todo
{
!) !!*
}
}

class TodoController
{
public function store(Request $request, CreateTodoAction $createTodoAction)
{
$todo = $createTodoAction(TodoData!$from($request));
}
}

As you can see, an action can be called just like a function. This is also a good option because actions look
different from any other class, so you immediately know this is an action.

However, there are cases when you cannot inject the action into a method but only into a constructor. For
example, when you're using an action in another one:

class CreateTodoAction
{
public function !&construct(
private readonly NotifyUsersAction $notifyUsersAction
) {}

public function !&invoke(TodoData $data): Todo


{
$todo = Todo!$create($data!%toArray());
($this!%notifyUsersAction)($todo);
}
}

31 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

As you can see, you need to wrap the action in parentheses. And I don't particularly appreciate how it looks,
so generally, this is not my go-to approach.

Static execute method

In this approach, all execute functions are static:

class CreateTodoAction
{
public static function execute(TodoData $data): Todo
{
!) !!*
}
}

class TodoController
{
public function store(Request $request)
{
$todo = CreateTodoAction!$execute(TodoData!$from($request));
}
}

This is my favorite one because it looks just neat! But if you want to write tests that mock actions, it's a poor
choice because you cannot do that easily (or maybe it's impossible, I'm not sure). However, it's not a
problem because I write API tests, so I don't mock actions. I only mock external dependencies, such as 3rd
party APIs. In this book, I'll go with this approach, and you'll see how clean the code looks.

You can be wrong if you think actions are not part of DDD and are just some modern "Laravel magic." I first
heard about actions from Robert C. Martin in his book "Agile Software Development," written in 2002 (10
years before Laravel was born). In this book, they were called "transactions." After that, I ran into them
again in the C# world, where they were called "commands." By the way, this is the first letter in CQRS (more
on that later). After all that, the MediatR package was created (2015), where actions were called "requests."
As you can see, actions have a long history, and the Laravel community did not invent them. However,
"action" is the coolest name I've ever heard to describe a class.

32 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

ViewModels
ViewModel is a very clever way to handle view-related data. But in this context, I'm not talking about Blade
views exclusively. You can think of a view model as a data container responding to a specific request. They
can be used in both SPAs (Inertiajs included) and full-stack MVC applications.

Let's say we work on a report page where we need to show revenue-related data, something like:

Total revenue
Total number of customer
The average revenue per customer

This example can use a (highly simplified) view model like this:

class GetRevenueReportViewModel extends ViewModel


{
public function totalRevenue(): int
{
return Order!$sum('total');
}

public function totalNumberOfCustomers(): int


{
return Order!$query()
!%groupBy('customer_id')
!%count('customer_id');
}

public function averageRevenuePerCustomer(): int


{
return $this!%totalRevenue() / $this!%totalNumberOfCustomers();
}
}

A view model implements (or calls from a query builder) every query that the page or response needs. It can
be used in controllers like this:

33 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class RevenueReportController extends Controller


{
public function index()
{
return new GetRevenueReportViewModel();
}
}

But how can we produce a JSON response from a class with methods only? The base ViewModel class
implements Laravel's Arrayable interface and will return an array like this:

[
'total_revenue' !( 24500,
'total_number_of_customers' !( 2311,
'average_revenue_per_customer' !( 10.60,
]

So a method called totalRevenue becomes a total_revenue array key, and Laravel will convert this array
to JSON. Later I'll show the exact code that does the magic (5 lines using Reflection).

Wait a minute! Earlier, you said we're going to use DTOs as responses, didn't you? Yes, that's true, so in the
sample application, instead of int values, I'll use DTOs. A quick example:

34 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class GetDashboardViewModel extends ViewModel


{
public function newSubscribersCount(): NewSubscribersCountData
{
return new NewSubscribersCountData(
today: Subscriber!$whereSubscribedBetween(
DateFilter!$today()
)!%count(),

thisWeek: Subscriber!$whereSubscribedBetween(
DateFilter!$thisWeek()
)!%count(),

thisMonth: Subscriber!$whereSubscribedBetween(
DateFilter!$thisMonth()
)!%count(),

total: Subscriber!$count(),
);
}
}

This is a dashboard page, and as you can see, I pack numbers together into a DTO, and I return this DTO
from the ViewModel. The response looks like this:

{
"new_subscribers_count": {
"today": 4,
"this_week": 39,
"this_month": 104,
"total": 341
}
}

35 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

ViewModels can help your project in two ways:

Your code is one step closer to the domain language. So when a product manager says "on the
dashboard page," you immediately know that they talk about the GetDashboardViewModel .
It can be an excellent addition to have the exact structure of your UI expressed as classes.

Later in the sample application, we will use Inertia but also APIs, so you will see how to write these classes
for both situations (spoiler: the same way).

Now that we've learned about actions and view models, we can move on to the mysterious CQRS.

36 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

CQRS
CQRS is one of the most overcomplicated and misunderstood aspects of DDD, in my opinion, so let's clarify
it! It means Command and Query Responsibility Segregation. It's a complicated name; let's see what those
words mean:

A command is a write operation such as creating a new product.


A query is a read operation such as getting all products.
Responsibility segregation means we need to separate these two types of functions.

So this simple class does not satisfy CQRS:

class ProductService
{
public function create(ProductData $data): Product
{
!)!!*
}

public function getAll(): Collection


{
!)!!*
}
}

I don't think there is anything wrong with this class; in fact, it can be the superior solution in many
projects.

So how do we satisfy CQRS? If you think about the previous sections, we have similar concepts to
commands and queries. The only difference is the name:

Actions play the role of a command or a write operation.


ViewModels play the role of a query or a read operation.

37 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

As you can see, it's very, very easy to achieve CQRS by using these two classes. As it turned out, I was using
CQRS years before I even knew what this thing was. So, I don't push the idea of CQRS; I like working with
actions and view models. Also, I don't think it's necessary for every application, but it gives you some
excellent benefits:

Your codebase becomes more understandable.


You have small, easier-to-maintain classes.
It gives you a perfect separation of concern.
Each class has one well-defined responsibility.

So that's what CQRS is all about. However, you'll find some very hard-to-understand articles and tutorials if
you search for them. They often show you how CQRS is used with other more complicated concepts such as
event sourcing, event stores, and separate read and write databases.

In this book, I won't talk about event sourcing for three reasons:

I'm not using it, so I'm not authentic to write about it.
I don't think it's necessary for most business applications.
It requires a whole different architecture and perspective.

So it's doubtful that you will start to refactor your application to event sourcing after learning about it.
However, it's much simpler to adapt any other concepts we'll use in this book.

CQRS can be a bit more complex (and usually is) than I described it. For example, in the C# world,
developers use a so-called mediator. It's a way to implement in-process synchronous messaging. Basically,
it's a layer that handles commands and queries. If you want to learn more about it, check out this package.

Also, CQRS is a very different (and complex) animal in the microservices world. It means you have separate
services and databases for reading and writing. In this context, it's almost required to use CQRS and event
sourcing and event-driven async architecture. If you're interested in this topic, check out this video (you can
watch it without a master's degree in CS).

38 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

States And Transitions


A State is a class that represents the state of something. That something, in most cases, is an Eloquent
model. So instead of a string, we have a dedicated class. What does a State class look like? Let's say we are
working on an e-commerce application, so we have an Order class. An Order has a status like:

Draft
Pending
Paid
PaymentFailed

For the sake of simplicity, let's say the most critical business logic about the Order's state is whether it can
be changed or not. A customer can modify a draft Order but cannot modify a Paid order.

First, let's create an abstract class:

abstract class OrderStatus


{
public function !&construct(protected Order $order)
{
}

abstract public function canBeChanged(): bool;


}

Each state extends the OrderStatus parent class. Now we can create these concrete classes:

class DraftOrderStatus extends OrderStatus


{
public function canBeChanged(): bool
{
return true;
}
}

As I said earlier a draft order can be changed but a paid cannot be changed:

39 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class PaidOrderStatus extends OrderStatus


{
public function canBeChanged(): bool
{
return false;
}
}

As I said earlier, a draft order can be changed, but a paid one cannot:

enum OrderStatuses: string


{
case Draft = 'draft';
case Pending = 'pending';
case Paid = 'paid';
case PaymentFailed = 'payment-failed';

public function createOrderStatus(Order $order): OrderStatus


{
return match($this) {
OrderStatuses!$Draft !( new DraftOrderStatus($order),
OrderStatuses!$Pending !( new PendingOrderStatus($order),
OrderStatuses!$Paid !( new PaidOrderStatus($order),
OrderStatuses!$PaymentFailed !( new PaymentFailedOrderStatus($order),
};
}
}

As you can see, this enum acts like a factory function. This is one of the hidden features of PHP8.1 enums. In
the Order model, we can leverage this with an attribute accessor:

40 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class Order extends Model


{
public function status(): Attribute
{
return new Attribute(
get: fn (string $value) !(
OrderStatuses!$from($value)!%createOrderStatus($this),
);
}
}

This is the new Laravel 8 accessor syntax; it's equivalent to this one:

public function getStatusAttribute(string $value): OrderStatus


{
return OrderStatuses!$from($value)!%createOrderStatus($this)
}

First, I create an enum from the string value stored in the database; after that, I call the factory on the
Enum. So anytime you access the status attribute of an order, you get an OrderStatus instance.

Now let's see how we can use these state classes:

class OrderController extends Controller


{
public function update(UpdateOrderRequest $request, Order $order)
{
abort_if(!$order!%status!%canBeChanged(), 400);
}
}

Or we can simply add a delegate method to the Order class:

41 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

class Order extends Model


{
public function canBeChanged(): bool
{
return $this!%status!%canBeChanged();
}
}

!) Now we can use it like:


$order!%canBeChanged();

!) Instead of:
$order!%status!%canBeChanged();

This structure can give you some advantages:

Encapsulation: Everything associated with a state is in one place.


Separation of concern: Each state has its class, so you have an excellent separation.
More simple logic: There is no need for nasty if-else or switch statements around a string attribute.

However, it can be a significant overhead if you have only a few states in your model and it's only used to
check some basic behavior.

Moving on, we need to change the state from Pending to Paid at some point. I think we all wrote code
similar to this in the past:

class OrderController extends Controller


{
public function pay(PayOrderRequest $request, Order $order)
{
!) Some logic here!!*
$order!%status = 'paid';
$order!%save();
}
}

Domain-Driven Design teaches us the following: we have to treat these transitions as first-class citizens. So
let's put them in dedicated classes! First, we can create some kind of abstraction. In this case, we don't need
a class, only an interface:

42 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

interface Transition
{
/**
* @throws Exception
!"
public function execute(Order $order): Order;
}

A Transition can be executed. It takes an Order and returns an Order. If something goes wrong it throws an
Exception. That's the contract.

This is a concrete Transition:

class DraftToPendingTransition implements Transition


{
public function execute(Order $order): Order
{
if ($order!%state!$class !!+ DraftOrderStatus!$class) {
throw new Exception('Transition not allowed');
}

$order!%status = PendingOrderStatus!$class;
$order!%save();

return $order;
}
}

First, it makes sure that the current Order is Draft and updates it to Pending. Also, you can use PHP8 enum
together with states and transitions.

As I said earlier, it can be beneficial if you have complicated logic based on states and transitions. However,
it can be overengineering and introduce unwanted complexity in your codebase.

For that reason, I rarely use them in my projects.

43 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Domains And Applications


The name Domain-Driven Design implies that a domain is something that drives the development process.
So it must be important, right? A domain is a “module” or a “container” that contains code that belongs
together. But the main criteria of this grouping are not technical at all. It’s all about real-world problems and
business language.

Laravel is a great framework and a good starting point for beginners. By default, it structures your code
based on technical details. I’m talking about this:

44 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

This is a tiny application, but you can see the code that handles Departments is spread across six different
places. It’s grouped by technical attributes:

Controllers
Models
Requests
Resources
Actions
DTOs

Imagine a project with 200 models, 300 actions, 500 controllers, and 500+ routes. After a while, it becomes a
nightmare. If you need to introduce a new attribute to the department model and accept it in your API, you
probably have to change five files in six different directories.

By the way, have you ever heard a stakeholder saying something like: “Please work a little bit on the
DepartmentController and add a new attribute to the DepartmentResource”? Of course not. They say things
like: “On the department page, I need to see the number of employees within the department.” And from
that information, you know that you probably need to tweak the DepartmentController and the
DepartmentResource.

A developer is someone who translates the business language into code.

So, in my opinion, it is a good idea if the code is similar to the business language and follows the same
structure. By using domains, we can achieve a much better structure:

45 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

The main benefit is that if you work on a broadcast-related feature, you go into the Broadcast folder, and
everything is there. Meanwhile, your co-worker can work in the Subscriber domain. It also makes
onboarding easier, in my opinion.

As you can see, there is no HTTP or Controllers folder inside a Domain. That's intentional. A domain
contains only business code. The subscriber folder contains everything you can do with a subscriber. It does
not have any application-related code. By application, I mean:

API
Web app
Console commands

These environment-related classes live inside the app folder, just like any default Laravel app.

An important note: by using domains we don't need to change anything framework-related. So there's no
need to tweak configs or bootstrap logic. Everything is untouched; Laravel upgrades won't be affected.

46 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Advantages And Disadvantages


We've discussed most of the essential technical concepts in Domain-Driven Design. To wrap things up:

Data

Value Objects
Data Transfer Objects
Working with data

Repositories (or custom query builder in Laravel)

Services

CRQS

Actions
View models
States and transitions

Code structure

Domains
Applications

If you think about them, these are straightforward ideas. Except for states and transitions, there's not even
a "pattern" or a class structure behind these concepts. They take an idea (like structuring unstructured data
with DTOs) and give us some classes that help us achieve the goal clean. This brings us to the most
important advantage:

The technical aspects of DDD are easy. We, developers, make it more complicated than it should be.

But these simple classes and ideas come with disadvantages as well:

There are a lot of different concepts and jargon associated with DDD. This is why it's often
misunderstood, and people make it more complicated than it should be.
Also, at the code level, we're going to use a lot of different classes. It can be hard to get used to it if
you're writing fat controllers and models.
As a bonus, if you're browsing DDD articles or videos online, they are often written in a highly abstract
way, so you need a Ph.D. just to understand them.

But here's the important thing:

In my view, these concepts are not strict rules. So it's not like "you either use them 100% correct, or
you're not event doing DDD." I don't think that's a good attitude.
For example, if you're confused about DTOs and value objects, pick one of them and put everything
under that folder. It's okay. You won't be outcasted from the developer community.

This book aims to help you write better software instead of blindly following some rules that don't fit in
Laravel.

Important note: there are other DDD concepts such as aggregates and aggregate roots that we won't use
in this book. The reason is simple: I don't think they are helpful with Laravel.

47 / 48
Martin Joo - Domain-Driven Design with Laravel Sample Chapter

Designing an E-mail Marketing Software


Most DDD articles or tutorials give you some abstract ideas and overcomplicated concepts and then
assume you're good to go. I want to do something different in this book. The best way to learn new ideas is
to build projects. So in the upcoming chapters, we're going to build a functional, complex e-mail marketing
system, just like ConvertKit or MailChimp. If you're not familiar with these applications, don't panic; in the
following pages, I will explain every important feature.

Overview
At its core, an e-mail marketing application is software that stores your subscribers, and you can send e-
mails to them. But it also comes with a great set of more complex features, such as:

Managing subscribers.
Tagging them. Tags are helpful to filter subscribers when you want to send e-mails.
Sending broadcast (or one-time) e-mails. You write the content, set the filters, and hit the Send button.
Creating sequences. A sequence is a collection of e-mails delivered to subscribers after a certain delay.
For example, you write four e-mails, and you want to send these e-mails in four weeks. You can create
a sequence that does precisely that automatically. And the great thing about sequences is that they
handle new subscribers as well. So if you create this sequence in February and a person subscribes in
May, they will still be added to it and get one e-mail per week.
Adding subscribers to sequences based on specific criteria. We can also create different filters. For
example, we want to exclude subscribers who bought a particular product or have a specific tag.
Creating subscription forms where people can enter their e-mail addresses. You can build an HTML
form with an e-mail input that can be embedded into your site. If someone submits this form, they will
be added to your e-mail list.
Tracking e-mail opens, and link clicks.
Generating reports from these metrics.
And much more, but these are the core features of ConvertKit.

In the upcoming pages, I'll discuss these features in more detail, and after that, we're ready to write some
user stories and design the domain model of our own ConvertKit Clone.

I hope you enjoyed this sample chapter; if you're still not sure if you want to purchase the 259-page book,
you can check out the following blog posts I wrote about domain-driven design:

Value Objects
Custom Query Builders
Data Transfer Objects
States And Transitions
Repositories
Actions
Domains And Applications

However, the book contains about 357% more knowledge than these articles and the sample chapter. You
can advance years in your career by reading it. If you purchase it and think this statement is BS, you’ll get a
refund with no question asked. You can find the details here.

48 / 48

You might also like