In this blog post, I will explain how I organized my last Symfony projects.I mainly use Hexagonal Architecture and CQRS. Keep in mind that I did not aim to implement these architectures strictly by the book. I only took concepts that helped me to create a straightforward and well-organized codebase.
Looking at the projectβs root, thereβs nothing particularly unusual. I kept all the folders and files generated during the Symfony installation.
tree . -L 1
βββ bin
βββ composer.json
βββ composer.lock
βββ config
βββ features
βββ public
βββ src
βββ symfony.lock
βββ tests
βββ translations
βββ var
βββ vendor
In the following sections, we will explore how I organized the application sources using Hexagonal Architecture and how CQRS helped me simplify the modeling write and read usecase.
My Approach to hexagonal architecture
The hexagonal architecture, or ports and adapters architecture, is an architectural pattern used in software design. It aims at creating loosely coupled application components that can be easily connected to their software environment by means of ports and adapters. This makes components exchangeable at any level and facilitates test automation.
https://en.wikipedia.org/wiki/Hexagonal_architecture_%28software%29
The main advantage of Hexagonal Architecture is that it decouples the heart of your application from Input/Output .
I call the heart of the application the Domain
. This is the area of the app where all the pieces of code represent the problem we are solving. This part must be side-effect-free, it must not rely on any tools, frameworks, or technologies.
Outputs
refer to the tools the application needs to work, such as network calls, database queries, filesystem operations, actual timestamps, or randomness. All Outputs
are moved to the infrastructure. Inputs
refer to how the domain is exposed to the outside world, for example, it can be a web controller or a CLI command. These pieces of code are moved to the UserInterface
.
Note: Check out my blog post about Hexagonal Architecture to dive deeper into the subject:
Based on this approach, my first decision was to split the src
directory into three areas: Domain
, Infrastructure
, and UserInterface
.
tree src/Domain/ -L 1
api/src/Domain/
βββ Domain
βββ Infrastructure
βββ UserInterface
Coupling rule:
Domain
must not depend on theInfrastructure
andUserInterface
.Infrastructure
andUserInterface
can depend on theDomain
.
I am not a big fan of Onion Architecture because I prefer to keep my projects as simple as possible. Having many layers can make maintenance challenging, as it requires aligning the entire team on coupling rules. Even agreeing with yourself can be difficult, so getting several people to agree is often much harder. Here, we follow just one simple rule : Domain
must not use IO
At times, I needed to create custom libraries because I couldnβt find any open-source libraries that met my expectations. To avoid coding directly in the vendor
directory, I introduced a third area called Libraries
(this area is optional). These libraries can be used in both the Domain
, UserInterface
and Infrastructure
layers, but their usage must not violate the coupling rules defined for those areas.
tree src/Domain/ -L 1
api/src/Domain/
βββ Domain
βββ Infrastructure
βββ Librairies
βββ UserInterface
Coupling rules: Libraries
must not depend on Domain
, UserInterface
and Infrastructure
Finally, I created a sub-area called Application within the Infrastructure layer. It contains all the code needed to have the application up and running, such as framework code (Symfony kernel and framework customizations), data fixtures, and migrations. In the following example, Exception
and Security
folders contain framework customizations.
tree src/Infrastructure/Application -L 1
api/src/Infrastructure/Application
βββ Exception
βββ Fixture
βββ Kernel.php
βββ Migrations
βββ Security
βββ Kernel
Note : Looking back, I won’t keep the folder in infra. All code related to framework customization should go into a dedicated folder called framework in the Libraries
folder, whereas Fixtures
and Migrations
can remain at the root of the infrastructure folder.
Focus on the business
A really important aspect for me is organizing the codebase around business concepts. I avoid naming folders and classes based on technical patterns like Entity
, ValueObject
, or Repository
, and especially not Provider
, DataMapper
, or Form
. Non-technical people should be able to understand the purpose of a class simply by its name.
Domain
tree src/Domain -L 1
api/src/Domain
βββ Cartographer
βββ Map
Since I avoided using technical terms to name folders, it’s easy to imagine that the project is about creating maps. Now, letβs take a look inside the Map
folder:
tree src/Domain/Map -L 1
βββ CartographersAllowedToEditMap.php // ValueObject
βββ Description.php // ValueObject
βββ MapCreated.php // Event
βββ MapId.php // ValueObject
βββ MapName.php // ValueObject
βββ Map.php // Root Aggregate
βββ Maps.php // Repository Interface
βββ Marker // All classes to design Marker entity
βββ MarkerAddedToMap.php // Event
βββ UseCase // Use cases orchestration
In this folder, we have all the code necessary to design the Map
aggregate. As you can see, I didnβt organize it by design patterns like ValueObject
, Entity
, or something else.
As you might have noticed, the Map
entity has a one-to-many relationship with the Marker
entity. All classes required to model this entity are located in the Marker
folder, which is organized in the same way as the Map
directory.
The UseCase
folder contains all the code needed to orchestrate use cases, such as commands, their handlers, and business validations.
Tip: I donβt suffix repositories with “Repository.” Instead, I try to use a business concept for the name, such as ProductCatalog
for a Product
aggregate. If I canβt find a suitable business concept, I use the plural form of the aggregate name, since a repository represents a collection of objects.
I organize the root of the Infrastructure
and UserInterface
folder in the same way as the Domain
one.
Infrastructure
tree src/Infrastructure -L 1
api/src/Infrastructure
βββ β¦
βββ Cartographer
βββ Map
βββ InMemoryMaps.php
βββ PostgreSqlMaps.php
UserInterface
tree src/UserInterface -L 1
api/src/UserInterface
βββ β¦
βββ Cartographer
βββ Map
βββ WebAddMarkerToMap.php
βββ CliAddMarkerToMap.php
My Approach to CQRS
Starting with Command Query Responsibility Segregation, CQRS is simply the creation of two objects where there was previously only one. The separation occurs based upon whether the methods are a command or a query (the same definition that is used by Meyer in Command and Query Separation, a command is any method that mutates state and a query is any method that returns a value).
The main idea of CQRS is the separation of the read and write sides. You can use different models for writing (commands) and reading (queries). I appreciate the concept of having two small, simple models dedicated to specific : purposes reading or writing instead of relying on one huge model. This approach helps prevent your aggregate from becoming a “god object,” which can happen as the system grows and more read and write use cases need to be handled.
Additionally, when you link aggregates by their IDs instead of direct references, complex read use cases can become challenging. How do you retrieve information from several aggregates? Itβs simpler to query the database directly rather than merging data from multiple aggregates.
Note: Check out my blog post to understand the difference between CQS and CQRS:
To manage having two models with the same name, I decided to split each subfolder of the domain into two areas: Command
and Query
. This structure allows me to design models with the same name, tailored to either reading or writing purposes.
tree src/Domain/ -L 2
api/src/Domain/
βββ Cartographer
β βββ Command
β βββ Query
βββ Map
βββ Command
βββ Query
Coupling rule: Command
area must not depend on the Query
area and vice versa.
Caution: Using CQRS, as defined by Greg Young, doesnβt mean introducing unnecessary complexity into your application. You donβt need a command and query bus, an event-sourcing architecture, or multiple databases to apply it. I chose to separate write and read use cases because it made my codebase simpler and clearer.
Last word
Iβve spent the last few years trying to find the perfect architecture, but Iβve realized it doesnβt exist. Instead, Iβve focused on using architectural concepts that make me and my teammates comfortable working on a daily basis. This project organization has been applied to multiple production projects. One of them is a side project I created for fun to build maps without relying on Google Maps. The others are a professional project that real people use daily.