How to define and enforce a code structure in layered architecture

Razvan Dubau
6 min readMar 11, 2024

--

I think there is not an unknown fact that during the lifespan of a large project many developers come and leave the team. Some of them bring with them a very good contribution, some are just doing their job and some of them… well, you got it. If your company is at the point where the business has reached a mature state and the application behind it evolves everyday, a clear set of rules would help the development team to stay consistent during every possible transition that might happen.

To provide some context, this article would help understand the topic better, but on short, we are talking about a furniture online store (PHP with Symfony) that wants to migrate their monolith to a more modern and flexible technology using DDD along with layered architecture.

Throughout my years of experience, I’ve observed that it’s very challenging to have all the developers adhere to the same set of rules and ensure that they all contribute to the codebase as efficient as possible.

Why saying efficient here?

If you have very strict code guidelines but don’t use the proper tools to enforce the rules, the company will end up losing a bigger part of their budget on rigorous code reviews and technical debt. The code review will never catch everything and if there are mistakes at the software architecture level in the early stages everything from there on will translate in more an more costs for the business. The costs will go into the technical debt.

Also poor design will increase the time needed for a future tasks to get done.

The short conclusion is that development will slow down gradually and so the code quality.

As a solution to this wide and abstract problem the team can use tools like phpstan (static code analyser), which checks for complexity and problems in the code mostly in the low level parts, like classes and functions. Other mandatory tool can be considered a code style fixer. I won’t get into details which one is better here, because I want to switch the focus to the architecture level and not on a single function or class.

In the layered architecture context the simplest and most common structure for the code is that you have 3 base layers: Application, Domain and Infrastructure.

On our furniture store we decided to go with 3 layers:

  • Application
  • Domain
  • Infrastructure

As a general rule what we want to achieve here, is a very modular and flexible code. By using DDD we certainly want to isolate the domain in a way or another and keep it there like a map of the real business.

The application layer is responsible for facilitating user interaction with the domain, serving as the intermediary between the user and the underlying business/domain logic.

The infrastructure has the role to ensure that the application and domain layers can do what they have to. For example: saving an order, grabbing data from a database or a third party API. We should think at this layer as a section of the software which is replaceable or changeable.

An example can be, consuming a third party API to grab delivery costs for each region, which at some point in time will change its public interface by renaming the route. The application and domain layer should not care about that! They should only know that there is a service somewhere that will provide that functionality. How the service is implemented and what it does behind the scenes is considered an implementation detail.

How can we do this?
We can achieve it by making the application layer to be dependent on an interface. Then, the infrastructure layer will have the duty to provide a concrete implementation for that interface.

What we should notice here is that the infrastructure service can always be changed and the application and domain will not be affected since they depend on the interface and not on the service itself.

The problems start to arise when developers start to use a class from infrastructure directly into the application layer. By doing it the application layer is now tight coupled with the infrastructure layer. To avoid this problem, we should inject the services only by the interface name and let the dependency injection resolve which concrete implementation should be chosen from the infrastructure layer.

Another example might be that one domain will use an object that belongs to another domain. Like Order will create a Delivery object. (cross domain dependency)

Example of a wrong coupling between layers

The fix for te cross domain dependency will be to remove the Delivery dependency and have the OrderCreated domain event dispatched to the network and let the Delivery domain listen to it and do its job.

For the Application <-> Infrastructure coupling issue we can make the CreateOrderHandler to depend on PersistsOrderInterface instead of OrderRepository. Dependency injection will decide which repository should be injected for this interface.

Example of a good coupling between Application and Infrastructure layers

It is enough to break one of these rules once. If that happens, for sue it will happen again and again.

Solution

We can use deptrac (qossmic/deptrac on github), a static code analyser, that helps to keep the layer or class interaction under control.

How it works:
You create a yaml config file specifying which layers can interact between them, set a pipeline on every merge request and you’re done!

In this way developers will be forced to understand the issue they are creating. Understanding it and with the power of habit their skills will improve. A win-win for both programmers and companies.

# deptrac.yml
imports:
- deptrac.baseline.yaml
parameters:
paths:
- ./src
layers:
# Domain layers
- name: OrderDomain
collectors:
- type: className
regex: App\\Order\\Domain\\.*
- name: DeliveryDomain
collectors:
- type: className
regex: App\\Delivery\\Domain\\.*

# Infrastructure layers
- name: OrderInfrastructure
collectors:
- type: className
regex: App\\Order\\Infrastructure\\.*
- name: DeliveryInfrastructure
collectors:
- type: className
regex: App\\Delivery\\Infrastructure\\.*

# Application layers
- name: OrderApplication
collectors:
- type: className
regex: App\\Order\\Application\\.*
- name: DeliveryApplication
collectors:
- type: className
regex: App\\Delivery\\Application\\.*

# Common collectors
- name: CommonApplication
collectors:
- type: className
regex: App\\Common\\Application\\.*
ruleset:
OrderInfrastructure:
- OrderApplication
- OrderDomain
OrderApplication:
- CommonApplication
- OrderDomain

DeliveryInfrastructure:
- DeliveryApplication
- DeliveryDomain
DeliveryApplication:
- CommonApplication
- DeliveryDomain
exclude_files:
- '#.*Tests.*#'

The above configuration will show the following report if we test the code from the screenshot containing the wrong coupling examples:

 ----------- --------------------------------------------------------------------------------------------------------------------------------------------------------------- 
Reason OrderApplication
----------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------
Violation App\Order\Application\CommandHandler\CreateOrderHandler must not depend on App\Order\Infrastructure\Doctrine\Repository\OrderRepository (OrderInfrastructure)
/Users/razvandubau/projects/furniture-demo/src/App/Order/Application/CommandHandler/CreateOrderHandler.php:18
Violation App\Order\Application\CommandHandler\CreateOrderHandler must not depend on App\Delivery\Domain\Delivery (DeliveryDomain)
/Users/razvandubau/projects/furniture-demo/src/App/Order/Application/CommandHandler/CreateOrderHandler.php:29
Violation App\Order\Application\CommandHandler\CreateOrderHandler must not depend on App\Delivery\Domain\Delivery (DeliveryDomain)
/Users/razvandubau/projects/furniture-demo/src/App/Order/Application/CommandHandler/CreateOrderHandler.php:6
Violation App\Order\Application\CommandHandler\CreateOrderHandler must not depend on App\Order\Infrastructure\Doctrine\Repository\OrderRepository (OrderInfrastructure)
/Users/razvandubau/projects/furniture-demo/src/App/Order/Application/CommandHandler/CreateOrderHandler.php:11
----------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------- -----
Report
-------------------- -----
Violations 4
Skipped violations 0
Uncovered 0
Allowed 18
Warnings 0
Errors 0
-------------------- -----

By removing the dependency on the Delivery class and also using PersistsOrderInterface the deptrac report will look like:

-------------------- ----- 
Report
-------------------- -----
Violations 0
Skipped violations 0
Uncovered 0
Allowed 18
Warnings 0
Errors 0
-------------------- -----

Conclusion

Deptrac is a potent tool that limits the interactions between layers and helps the team to be consistent code wise, and keep the boundaries clean.

The tool itself is for PHP, but if you read until here and you’re working with a different language, don’t get discouraged as the concept stay the same. Most probably there are similar tools for other programming languages.

Note: You can see in the deptrac.yml file that and in the code screenshot that both domains, Order and Delivery, are in the same codebase. That shouldn’t be a problem since the domains don’t depend on each other. They can easily be separated into independent micro-services by extracting the domain code (Application, Domain and Infrastructure) along with the common helpers.

--

--

Razvan Dubau

Software developer that focuses on clean code and scalable solutions