Onion architecture is one of the two known “clean” software architectures. The other “clean” software architecture is widely known as Ports and Adapters pattern or Hexagonal architecture. Both makes an explicit separation on what belongs in the application core and what belongs outside such as databases, user interfaces and 3rd-party APIs.
It is a software architecture introduced by Jeffrey Palermo back in 2008 with his four-part series called The Onion Architecture. Like the Layered Architecture and Hexagonal Architecture, it uses the concept of layers but the difference lies in the following:
- Domain Model layer – part of the domain layer where our entities and classes closely related to them e.g. value objects reside
- Domain Services layer – part of the domain layer where domain-defined processes reside
- Application Services layer – where application-specific logic i.e. our use cases reside
- Outer layer (Infrastructure, Interfaces, Tests) – which keeps peripheral concerns like UI, databases or tests
In this article, you will get to see a set of rules that have been very helpful for me when I apply onion architecture in my software projects. Every layer has their respective rules and are categorised as such. These rules allowed me to focus on solving the domain problem and reduce the need to think about what codes should go where. I get increased productivity while also having structured flexibility in my codebase.
Now, some of these rules are derived based from what I have understood about the architecture while some of these rules have been developed by other expert software developers. There are also some rules that are not specific to onion architecture, but rather, just innate to the specific software pattern.
Let us dive in…
These rules are applicable to the whole application or software module.
Do not skip layers when calling methods or utilising functions that are in the deeper layers. Typical flow of execution is as follows:
Interfaces -> Application -> Domain or Infrastructure
Use static methods and classes as a last resort.
Use a dependency injection framework to implement the onion architecture.
The interface layer only contains codes that handle the following:
- Deserialisation of incoming objects sent via API request/call.
- Serialisation of objects or messages for the purpose of responding to an API request/call.
- Exposure and implementation of RESTful API and SOAP-based web services
This is the topmost layer in the onion architecture that can work with a domain object such as aggregate root or entities directly.
Data transfer objects (DTO) are to be used when receiving data from an API or responding to an API request as they define the data contracts. Never use domain entities (e.g. aggregate root and value objects) to receive or return data via an API.
Use a facade to provide a common entry point for multiple endpoints (RESTful API, SOAP and direct function call) if they need to consume a service provided by the application layer.
Facades do not contain any business or domain logic.
Application Service Layer
The Application layer contains only codes related to the following:
- Coordination between domain objects, services and utilities.
- Database transaction control
- Establishing connections to databases
- Application control or startup (e.g. main function/main class)
The Application Service layer is only concerned with the software use cases. Each method or function in a service class typically represents one use case.
Classes in the application layer never hold and maintain the state of any domain entity. The only type of state allowed for the application service layer are transaction state.
They handle injection of repositories into domain services that need them to function.
Application services do not contain any business logic.
Application services typically do not return anything with the exception of query services
Domain entities such as aggregate root or entities do not know anything about storage and do not work directly with repositories even if they are injected as parameters to the method of an entity.
Aggregate roots and entities are not allowed to exit the Interface or Infrastructure layer.
Repositories generally deal with storage such as file or databases but they exists only as interfaces with methods that follows the ubiquitous language. Implementations are done in the infrastructure.
Services in the domain layer exists only if and only if there is a need for operations that does not quite fit into an aggregate root or entity. Never create unnecessary services when domain entity or aggregate root can handle it internally.
Value objects are to be used to return immutable data or represent a state change in the domain.
The infrastructure layer contains codes related to the following:
- Actual implementation of repositories to make use of ORM frameworks such as Hibernate, Entity Framework or call databases directly.
- Consumption or utilisation of external or 3rd party APIs, and the mapping and translation of external models to domain models.
- Highly technical implementation of services that are required by the domain such as encryption, document processing and image processing.
This is the bottommost layer that can work directly with domain objects.
These rules serve as guidelines for software developers when they need to work with onion architecture and are by no means exhaustive. And they are only effective if the developers themselves are disciplined enough when it comes to applying the rules in their work.
And, more expert developers may have differing opinions or they have additional rules or principles that they discovered to be very helpful during the development and implementation phase. Therefore, if there are corrections to be made, do leave a comment below and I will update the information here. This way, all of us get to benefit and improve the general quality of software.