Hexagonal Architecture + DDD + CQRS by Example Book code repository
Cheeper is a Twitter clone used in the book CQRS By Example as a reference implementation. Here you will find an example of applying Hexagonal Architecture, DDD (tactical patterns) and CQRS (Command query responsibility segregation) in a real use case PHP application.
Take a look around, play a bit with the code, and if you like what you see ...
Give us a star
Table of contents
- About the book
- Code structure
- Running the application
About the Book
Command-Query Responsibility Segregation is an architectural style for developing applications that split the Domain Model in Read and Write operations in order to maximize semantics, performance, and scalability. What are all the benefits of CQRS? What are the drawbacks? In which cases does it worth applying it? How does it relate to Hexagonal Architecture? How do we properly implement the Write Model and Read Models? How do we keep in sync both sides? What are the following steps to move towards Event Sourcing? This book will answer all these questions and many more, guided through lots of practical examples. Brought to you by the same authors behind "Domain-Driven Design in PHP".
Do you want to know more? Do you want to grasp further details about CQRS?
Code Structure
This repository follows a typical PHP application layout: All the code lives in the src
folder and classes map 1:1 with the file where they live, following the PSR-4 standard for class autoloading.
App
namespace
The This namespace contains all the classes that are specific to the delivery mechanism. In this case the delivery mechanism is the Symfony framework following the recommended structure.
Cheeper
namespace
The The Cheeper
namespaces is a bit little different from a typical PHP application using tactical Domain-Driven design patterns, where you could expect several modules under the Cheeper
namespace. In here you will find book-chapter namespaces instead of modules, so it's easy for you to navigate through all the code of each chapter.
src/Cheeper
โโโ AllChapters
โ โโโ Application
โ โโโ DomainModel
โ โโโ Infrastructure
โโโ Chapter2
โ โโโ Author.php
โ โโโ Cheep.php
โ โโโ Hexagonal
โ โโโ Layered
โ โโโ Spaghetti
โโโ Chapter4
โ โโโ Application
โ โโโ DomainModel
โ โโโ Infrastructure
โโโ Chapter5
โ โโโ Application
โ โโโ Infrastructure
โโโ Chapter6
โ โโโ Application
โ โโโ Infrastructure
โโโ Chapter7
โ โโโ Application
โ โโโ DomainModel
โ โโโ Infrastructure
โโโ Chapter8
โ โโโ Application
โ โโโ DomainModel
โ โโโ Infrastructure
โโโ Chapter9
โโโ DomainModel
โโโ Infrastructure
Running the application
Requirements
- Docker
๐ To run all services in your computer without installing all the dependencies. - Docker Compose
๐ To orchestrate the services. If you're on Mac or Windows and you have installed Docker Desktop you already have Docker Compose installed. If you're on Linux, you can check out installation here. - HTTPie
๐ To make HTTP requests in the same way as they're done in this README file.
Getting started
The first thing you should do is to have a fresh copy of the code. To do so just clone the repository
git clone https://github.com/dddshelf/cheeper-ddd-cqrs-example
Then, in order to get environment set up properly, you should have a .env.local
file with all the correct values
cp .env.docker.dist .env.local
And you're ready to go!
Set all the infrastructure up
As the code runs fully on docker, several make
targets have been carefully prepared in order to make the interaction with the code as easy and smooth as possible. To start all the services just run
make start
This make
target, runs docker compose, and ensures all services are up and running. Next, once all the services have been started, we must make sure the database and queues start empty and database migrations are executed successfully. To do so, just run
make infrastructure
Finally to stop all services just run
make stop
Now you're ready to execute all the demo steps!
Demo time!
Cheeper starts totally as a blank application. There is no Author, Cheeps, Follows, etc.
1. Follower Counters Query
http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/followers-counter
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/followers-counter
http --json --check-status --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/followers-counter
Expected output
{
"data": {
"message": "Author \"a64a52cc-3ee9-4a15-918b-099e18b43119\" does not exist"
},
"meta": {
"message_id": "c0342dfe-782e-4ffa-9e9c-8d6d82533ab2"
}
}
{
"data": {
"message": "Author \"1fd7d739-2ad7-41a8-8c18-565603e3733f\" does not exist"
},
"meta": {
"message_id": "35089f63-53ca-4a44-9a7c-6e588ba6d885"
}
}
{
"data": {
"message": "Author \"1da1366f-b066-4514-9b29-7346df41e371\" does not exist"
},
"meta": {
"message_id": "d07a6984-abc9-4a3a-92bb-829ad5dd85df"
}
}
2. Author Timeline
http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/timeline
Expected output
{
"_meta": [],
"data": "Author \"1da1366f-b066-4514-9b29-7346df41e371\" does not exist"
}
3. Adding Authors
http --json --body POST http://127.0.0.1:8000/chapter7/author author_id="a64a52cc-3ee9-4a15-918b-099e18b43119" username="bob" email="[email protected]"
http --json --body POST http://127.0.0.1:8000/chapter7/author author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" username="alice" email="[email protected]"
http --json --body POST http://127.0.0.1:8000/chapter7/author author_id="1da1366f-b066-4514-9b29-7346df41e371" username="charlie" email="[email protected]"
Expected output
{
"author_id": "a64a52cc-3ee9-4a15-918b-099e18b43119",
"message_id": "d3c90181-c2c1-4e5c-90e9-99a799197b88"
}
4. Consuming Events
php bin/console messenger:consume events_async
Expected output
08:59:13 INFO [messenger] Received message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO [messenger] Message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned handled by Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned","handler" => "Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle"]
08:59:13 INFO [messenger] Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned was handled successfully (acknowledging to transport). ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO [messenger] Received message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO [messenger] Message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned handled by Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned","handler" => "Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle"]
08:59:13 INFO [messenger] Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned was handled successfully (acknowledging to transport). ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO [messenger] Received message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
08:59:13 INFO [messenger] Message Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned handled by Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned","handler" => "Cheeper\Chapter7\Infrastructure\Application\Author\Event\SymfonyNewAuthorSignedEventHandler::handle"]
08:59:13 INFO [messenger] Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned was handled successfully (acknowledging to transport). ["message" => Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned^ { โฆ},"class" => "Cheeper\Chapter7\DomainModel\Author\NewAuthorSigned"]
5. Follower Counters Query
http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/followers-counter
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/followers-counter
http --json --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/followers-counter
Expected output
{
"_meta": {
"message_id": "8db6fc1d-41d5-4f6b-8950-e5766eee53e9"
},
"data": {
"authorId": "1da1366f-b066-4514-9b29-7346df41e371",
"authorUsername": "charlie",
"numberOfFollowers": 0
}
}
6. Author Timeline again
http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/timeline
Expected output
{
"cheeps": []
}
7. Following other Authors
http --json --body POST http://127.0.0.1:8000/chapter7/follow follow_id="8cc71bf2-f827-4c92-95a5-43bb1bc622ad" from_author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" to_author_id="a64a52cc-3ee9-4a15-918b-099e18b43119"
http --json --body POST http://127.0.0.1:8000/chapter7/follow follow_id="f3088920-841e-4577-a3c2-efdc80f0dea5" from_author_id="1da1366f-b066-4514-9b29-7346df41e371" to_author_id="a64a52cc-3ee9-4a15-918b-099e18b43119"
8. Consuming events again
php bin/console messenger:consume events_async
9. Follower Counters Query
http --json http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/followers-counter
http --json http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/followers-counter
http --json http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/followers-counter
Expected output
{
"_meta": {
"message_id": "0e9df870-894f-4d37-a140-4d598941fa4f"
},
"data": {
"authorId": "a64a52cc-3ee9-4a15-918b-099e18b43119",
"authorUsername": "bob",
"numberOfFollowers": 1
}
}
10. Posting Cheeps
http --json --body POST http://127.0.0.1:8000/chapter7/cheep cheep_id="28bc90bd-2dfb-4b71-962f-81f02b0b3149" author_id="a64a52cc-3ee9-4a15-918b-099e18b43119" message="Hello world, this is Bob"
http --json --body POST http://127.0.0.1:8000/chapter7/cheep cheep_id="04efc3af-59a3-4695-803f-d37166c3af56" author_id="1fd7d739-2ad7-41a8-8c18-565603e3733f" message="Hello world, this is Alice"
http --json --body POST http://127.0.0.1:8000/chapter7/cheep cheep_id="8a5539e6-3be2-4fa7-906e-179efcfca46b" author_id="1da1366f-b066-4514-9b29-7346df41e371" message="Hello world, this is Charlie"
11. Author Timeline
http --json --body http://127.0.0.1:8000/chapter7/author/a64a52cc-3ee9-4a15-918b-099e18b43119/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1fd7d739-2ad7-41a8-8c18-565603e3733f/timeline
http --json --body http://127.0.0.1:8000/chapter7/author/1da1366f-b066-4514-9b29-7346df41e371/timeline