Elevate Your Symfony Projects: Implementing DDD, CQRS, and Hexagonal Architecture

Jakub Skowron (skowron.dev)
6 min readSep 12, 2023

--

Photo by ThisisEngineering RAEng on Unsplash

Introduction

Software development is not just a technical matter, but also understanding the business for which we create solutions. Domain Driven Development (DDD) is the answer to these challenges, offering an approach that focuses on modeling the core business logic. In this article, we will take a closer look at DDD, its benefits, and practical ways of implementing it in Symfony 6.

What is Domain Driven Development (DDD)?

DDD is an approach to software design that focuses on modeling business reality. A key element in DDD is the “ubiquitous language”, which allows for better understanding and communication between the business and developer teams. The idea of the DDD approach (acronym for Domain — Driven — Design) is very simple: align all the teams involved in the development of a product:

  • Achieve fluid and two-way communication between the business team and the development team.
  • Ensure that the developed product meets business expectations.
  • Facilitate the iterability and maintainability of the product.

The underlying idea when we implement DDD is to separate the domain of our application as much as possible from the infrastructure we use to implement the project (“separation of concerns”). That is, if we are using Symfony to develop the project, it is desirable that our domain entities are completely unaware of the existence of the ORM Doctrine or any of the components that Symfony offers us to implement functionality.

Components of DDD:

  1. Model/Domain: Representation of our domain in code. The model contains all the key concepts and business rules of our application. A Domain is a “sphere of knowledge”, for instance the business the company runs. A Domain is also called a “problem space”, so the problem for which we have to design a solution.
  2. Aggregates: A set of objects that are treated as a single unit for data modification purposes. Each aggregate has a main object, called the aggregate root, through which all interaction with the aggregate takes place.
  3. Bounded Contexts: The boundary within which a specific model makes sense. In one bounded context, certain words may have a different meaning than in another context.

Advantages and Disadvantages of DDD

Advantages:

  1. Increased communication between teams: Thanks to a common language, business and developer teams can communicate better and understand each other’s needs. This common language eliminates misunderstandings and allows for more effective collaboration.
  2. Better understanding of the domain: DDD allows developers to gain a deeper understanding of the business problems they are trying to solve. This deeper understanding leads to the creation of more precise and effective solutions.
  3. Flexibility: By focusing on the domain, the code becomes more flexible and easier to modify as business requirements evolve. This flexibility allows for faster adaptation to changing market needs.

Disadvantages:

  1. Higher initial effort: Implementing DDD initially requires more time and commitment from the entire team than traditional approaches. This requires an investment of time in the initial stages of the project but pays off in the long run.
  2. Complexity: DDD introduces additional complexity in the form of new patterns and concepts that the team must understand and adopt. However, this complexity is necessary to achieve a deeper understanding of the domain.

CQRS

CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read operations from write operations. This allows us to scale and optimize both parts of the system independently. CQRS allows for independent scaling and optimization of read and write operations, which is especially useful in large and complex systems. This pattern also promotes clarity and unambiguity in the code, as read and write operations are clearly separated. This allows teams to focus on optimizing each of these operations independently, leading to more efficient and scalable solutions.

Sample code for CQRS:

// Write command
class CreateUserCommand {
private $userId;
private $username;
// ...
}

// Write command handler
class CreateUserHandler {
public function __invoke(CreateUserCommand $command) {
// logic for creating a user in the database
}
}
// Read query
class GetUserQuery {
private $userId;
}
// Read query handler
class GetUserHandler {
public function __invoke(GetUserQuery $query) {
// logic for reading a user from Elasticsearch
}
}

Hexagonal Architecture

Hexagonal Architecture, also known as Ports and Adapters, is an architectural pattern that promotes separating the business logic of an application from its external layers, such as a database or user interface. This makes the business logic more modular and easier to test. Hexagonal Architecture allows for the isolation of business logic from external dependencies, making it easier to test and develop applications. This pattern also promotes modularity and flexibility, making applications easier to maintain and expand.

Sample code for Hexagonal Architecture:

// Port
interface UserRepository {
public function save(User $user);
public function findById($userId);
}

// Database adapter
class DatabaseUserRepository implements UserRepository {
public function save(User $user) {
// logic for saving a user in the database
}
public function findById($userId) {
// logic for reading a user from the database
}
}
// Elasticsearch adapter
class ElasticsearchUserRepository implements UserRepository {
private $client;
public function __construct(Client $client) {
$this->client = $client;
}
public function save(User $user) {
$params = [
'index' => 'users',
'id' => $user->getId(),
'body' => [
'username' => $user->getUsername(),
// ... other user fields
]
];
$this->client->index($params);
}
public function findById($userId) {
$params = [
'index' => 'users',
'id' => $userId
];
$response = $this->client->get($params);
return new User($response['_source']['username']);
}
}

Configuring Symfony Messenger

To configure symfony/messenger in Symfony, we need to add the appropriate settings in the config/packages/messenger.yaml file:

framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'App\Command\CreateUserCommand': async
'App\Query\GetUserQuery': async

In the above configuration, we have defined the async transport, which can be used for asynchronous processing of commands and queries. We then define routing for our CreateUserCommand command and GetUserQuery query, indicating that they should be processed asynchronously.

Configuring Elasticsearch in Symfony

To configure Elasticsearch in Symfony, we first need to install the appropriate Elasticsearch client library using composer:

composer require elasticsearch/elasticsearch

Then, we can configure the Elasticsearch client in the config/services.yaml file:

services:
Elasticsearch\Client:
arguments:
$config:
hosts: ['localhost:9200']

With the above configuration, when injecting the Elasticsearch\Client dependency into our service (e.g., ElasticsearchUserRepository), Symfony will automatically provide us with a configured instance of the Elasticsearch client.

Is DDD “a silver bullet”?

No, DDD is not always the optimal solution — this approach takes a lot more time and more involvement from the entire team than normal development. In addition, transferring the business domain to software requires following certain standards that allow those ideas to be ported to the entities and services of the application. Stick with this idea:

DDD encourages us to bring business logic as close as possible to our domain entities.

Therefore, before launching to implement a project with a domain-based approach, make sure that you have the appropriate time and involvement.

Conclusion

Domain Driven Development is an approach that allows for a better understanding and modeling of business reality. Combined with modern architectural patterns, such as CQRS and Hexagonal Architecture, and tools available in Symfony, DDD becomes an even more attractive approach to application development.

--

--

Jakub Skowron (skowron.dev)

Poland based PHP/Python Web Backend dev. Love to work with Symfony and FastAPI frameworks. In spare time totally gearhead.