Architecture
Table of contents
Loading...Introduction
In order for big projects to stay maintainable and scalable, it is crucial to isolate specific
responsibilities into separate layers. That way, each part can be managed and updated
independently without causing cascading effects throughout the entire application.
The task of each layer is clearly defined, what it needs as parameters and what it returns.
There are multiple ways to structure a system, and every article on the internet seems to describe layers with similar names in totally different ways.
The slim-example-project, slim-api-starter and slim-starter, use three main layers inspired by the Domain Driven Design (DDD): Application, Domain and Infrastructure.
Application
The Application layer is the top-most layer and contains the code that is responsible
to deal with everything going out to the client or coming into the application after
the front-controller.
It contains middlewares,
handles HTTP requests (Action),
handles errors and returns the HTTP response.
The Application layer is responsible for orchestrating the execution of business operations, delegating the actual business logic to the Domain layer.
Domain
The Domain
layer is the heart of the application encapsulating the core business logic
and rules.
Essential concepts and behaviours for the application to run as desired are defined here.
It contains the service classes, exceptions and is responsible for the
interaction with the infrastructure layer.
Infrastructure
The Infrastructure layer is the bottom-most layer and is responsible for the communication
with external dependencies such as the database via repositories, the file system, and the mail server.
It needs to be separated from the domain layer to increase testability and facilitate
the replacement of adapters to external dependencies with other implementations.
This layer should not contain logic and not be aware of the domain layer (but
may create
and return DTOs from the Domain).
Layer separation
Clean architecture
The classic approach would be to have three main folders inside the project directory src
to clearly separate the layers Application, Domain and Infrastructure
(e.g. Slim-Skeleton).
Everything Application-related resides in the src/Application
folder, Domain-related
stuff in the src/Domain
folder, and the repositories for each module / feature would be in the
src/Infrastructure
folder.
Example directory structure:
├── src
├── Application
│ ├── Module1 # (e.g. Authentication)
│ │ ├── Action
│ ├── Module2 # (e.g. Client)
│ │ ├── Action
│ └── etc.
├── Domain
│ ├── Module1 # (e.g. Authentication)
│ │ ├── Service
│ ├── Module2 # (e.g. Client)
│ │ ├── Service
│ └── etc.
└── Infrastructure
├── Module1 # Authentication
│ ├── Repository
├── Module2 # Client
│ ├── Repository
└── etc.
The issue with this approach
Having the layers separated first and then the modules in each layer makes it unnecessarily more difficult to maintain and keep an overview, especially for projects that have lots of folders and modules.
If a new feature or module is added or changed, the developer has to jump between
the Application, Domain and Infrastructure folders to make the changes.
Scrolling between those different "layer-folders" and searching the right module folders in each layer
is not efficient in the practical world.
There is a great solution to this, and it's called the vertical slice architecture.
Vertical slice architecture
Instead of separating the layers first and then having a module folder in each layer,
the vertical slice architecture suggests having the module folders in the same parent folder (e.g. src/Module
)
and separate the layers inside each module folder.
Since modules typically contain multiple features or "use cases", we can go even further and separate the layers
inside each feature folder.
Every feature is now a "Slice" containing all the layers by itself. This is fantastic to keep the codebase organized and maintainable, as each feature of every module is mostly self-contained and independent.
To have a more concise and less cluttered codebase, the layer folder names ("Application", "Domain", "Infrastructure") can be omitted if it doesn't serve a grouping purpose (e.g. if the Domain only contains a services or the Application only actions). For this to work, the developer must be aware that the contents of folders like "Service", "Action" or "Repository" inherently belong to different layers.
The three layer folders still exist under src/
but are only used for general utilities,
exceptions, and services that are used by the application as a whole and not tied to a specific module.
The /src
directory structure would look like this now:
├── src
├── Application # general (not tied to specific module) top layer
│ ├── Middleware
│ └── Responder
├── Common # contains general project independent utility classes
├── Domain # includes general business logic utilities and exceptions
│ ├── Exception
│ └── Utility
├── Infrastructure # contains general infrastructure utilities such as query factory
│ ├── Factory
│ └── Utility
└── Module
└── {ModuleX}
├── Create # Create feature / use case
│ ├── Application
│ │ ├── Action # Single Action Controller
│ │ └── Exception # Exceptions that belong to the Application layer
│ ├── Domain
│ │ ├── Exception # Domain exceptions
│ │ └── Service # ClientCreator service
│ └── Infrastructure
│ ├── Exception # Infrastructure exceptions
│ └── Repository # ClientCreatorRepository
├── Data # DTOs
├── Delete # Delete feature
│ ├── Action # Action folder without named parent Application directory
│ ├── Service # Service folder without named parent Domain directory
│ └── Repository # Repository folder without named parent Infrastructure directory
├── Read
│ ├── Action
│ ├── Service
│ └── Repository
├── Update
│ ├── Action
│ ├── Service
│ └── Repository
└── Validation
└── Service # Shared validation service
The Directory-Structure contains an overview of the entire project folder structure.
Flowchart
Loosely coupled modules
Modules should be loosely coupled, meaning that they should not or only slightly dependent on each other. Whenever possible, modules should be completely independent and not share any component as this has many advantages. It's mainly easier to refactor and maintain in the long run as changes can be made without worrying about affecting other modules.
When modules share elements
If multiple modules need an element from another Module, there are two options:
- The element is extracted into a separate module folder on the same level as the other modules.
- An example for this is Authorization, which contains Enum, Service, Exception and Repository used in multiple modules and thus has its own Module folder.
- Keep the element in the Module it belongs to and use it in the other modules. This makes the
modules that use this element dependent on the module that stores it.
- An example where this makes sense for me would be the
UserRole
andUserStatus
Enum stored in the User Module but used by multiple other Modules.
- An example where this makes sense for me would be the
I'd say the main factors that influence such a choice are
- Can the element / use case be clearly tied to one Module, or is it not very specific to one Module?
- How many elements are concerned, is it an entire feature with complex logic and database access or just an Enum or a simple Service?
- Could sibling elements or features be required in the future that could be grouped with the shared element(s)?
- Does it feel like it makes sense to have it in an own Module?
Extracting the shared elements into a separate Module folder is probably always better in terms of
SOLID principles, but it has the disadvantage of bloating src/Module
with many folders.
It's a compromise, but I deem it acceptable and practically cleaner to keep the shared elements in the Module,
as long as they don't do too much, and really belong to that Module.
At the end of the day, it's a subjective and situational decision which ultimately depends on how the developer feels most comfortable.
Features inside a module
The modules contain multiple features / use cases which have their own Application, Domain and Infrastructure layers, as per the vertical slice architecture.
Features should also be designed to be loosely coupled and mainly independent of each other.
When features share elements
If multiple features share the same functionality (e.g. Validation), it should be extracted into a separate feature folder. This is to have the feature only be responsible for one specific task which allows for a clear separation of concern (SRP).
Services that can be tied to one feature and are used by other features of the same module may be kept
in the feature folder under certain circumstances if it feels alright.
E.g. client-create authorization checker in the Client/Create
folder is also used by the client list
page feature (to determine if the "Create client" button should be displayed or not).
Data Transfer Objects (DTOs)
The Data
folder contains the DTOs and is layer-independent.
It is stored in the module's folder if it's used by different features or in the feature's subfolder if it
only belongs to a specific feature.
Folder creation
Folders are generally only created if they bring value.
If a feature slice only contains Service (or something else) from the domain layer, the Domain folder is omitted.
The same goes for Repository (or else) in the Infrastructure layer and Action (or else) in the Application layer.
This is to prevent unnecessary nesting and to keep the codebase as clean as possible.
However, the developer
should be aware that the components belong to a different layer.
If another component of the same layer is added, they must be grouped by a folder with the layer name.
E.g. if the src/Module/{moduleX}/{FeatureY}/Service
now contains a custom Domain Exception,
a Domain folder should be created and both the Service and Exception moved there:
src/Module/{moduleX}/{FeatureY}/Domain/Service
and src/Module/{moduleX}/{FeatureY}/Domain/Exception
.
At least one level of grouping in the feature folder is necessary (classes should always be in a folder e.g. Action, Repository, Service, Exception, Enum, Data etc.) to keep the codebase intuitive and uniform.
Module folder structure
The folder structure of a module is as follows:
├── {ModuleX}
│ │ ├── Data # DTOs
│ │ ├── Feature1
│ │ │ ├── Application
│ │ │ ├── Domain
│ │ │ └── Infrastructure
│ │ ├── Feature2
│ │ │ ├── Action # short /Action if the Application layer only contains Action
│ │ │ ├── Service # short /Service if the Domain only contains Service
│ │ │ └── Repository # short /Repository if the Infrastructure only contains Repository
More on the Vertical Slice Architecture:
- Vertical Slice Architecture, not Layers! - Code Opinion - YouTube
- Vertical Slice Architecture in ASP.NET Core - CodeMaze