Target Software Architectures: The Onion Architecture
What it is and how to implement it in your application
A Target Architecture provides the details of a future state of a software architecture being developed within an organization.
Today I'd like to talk about a special variant of the Hexagonal Architecture, which I personally find more practical: the Onion Architecture.
The Onion Architecture was proposed by Jeffrey Palermo in 2008.
Hexagons vs Onions
Whilst the Hexagonal Architecture has really no layers necessarily and talks about ports and adapters (which in my opinion they are kind of an inflexible concept), the Onion Architecture encourages you to do a better separation of concerns and good use of the Dependency Injection (DI) and Inversion of Control (IoC) patterns from the start.
In the Onion Architecture, the dependencies are always pointing inwards. The inner layer is the Domain Model and the outer one is the Infrastructure layer, which takes care of communicating with the external world.
Between the Domain Model and Infrastructure layers, you can have as many layers as your application or team needs, but in this article, I will only cover the Application and Presentation layers additionally.
As you can see in my proposal, the Presentation layer shares the same "level" as the Infrastructure one. Why is that? Because of the golden rule.
The Golden Rule
The golden rule is pretty simple: the outer layers can use elements from the same layer or inner layers, but not the other way around.
On the diagram, Presentation cannot be at a lower level than Infrastructure or vice-versa because they cannot use each other.
Code is always coupled towards the center which is the Domain Model and, since the Domain Model is the center, it can be only coupled to itself.
The parts of the software that are more subtle to change are or that we have less control about are in the most outer layers, while in the inner layers we have the most meaningful parts of our application.
The Onion Architecture relies heavily on the Dependency Inversion principle: the Domain Model (the core) needs implementation for its interfaces, and if those implementing classes reside at the edges of the application, we need some mechanism for injecting that code at runtime.
If you are a PHP developer like me, you'll know that Symfony is the most indicated framework to accomplish that.
The Domain Model Layer
The first and most important layer is the Domain layer, the inner most one. This is the life of your application. It represents your app's domain, the business logic and its functional implementation, everything it can do.
Classes (that we will call services from now on) created in this layer are business-logic related: e.g. a service that defines all operations we can do on an entity.
It consists of the following types of objects:
- Domain Services: Each component of the domain represents a private service that matters only within the domain, e.g. a state machine manager.
- Interfaces that represent the outside world
- Value Objects (a.ka. DTOs or classes with no business logic, only properties)
- Repository Interfaces
- Entities / Domain Objects (similar to Value Objects)
- Object Factories
- Domain Events, e.g.: an order got paid.
- Domain-level Exceptions
All these types of objects together represent the business logic of the project.
Domain Model Layer Rules
This layer MUST NOT have interaction with the outside world or any other layer. That means that code from the other layers MUST NOT be used here.
The only responsibility of the domain layer is to define how your business work, not how data is queried or persisted, that's why we SHOULD NOT use repositories directly here but define and use their interfaces instead.
Database layer abstractions occur in the Infrastructure layer, the Domain Model only defines its interfaces, which is the one the business code should always refer to when writing strict type-hinting.
The Application Layer
The application layer is where all our application features or "use cases" live.
It basically wraps around the domain layer, adding specific business rules on top (e.g. how much fees should the app charge to a customer, etc).
These are features and rules that are not necessarily part of the Domain Model, but that define the app's business. This layer is also called "Domain Rules" or "Domain Services".
May consist of the following types of objects:
- Use Cases or Application Services: wrap around the domain layer, adding extra logic or conditions specific to the use case.
- Application Events, something that happen internally in the application itself, e.g.: a specific method was called, the app has started/finished doing something, etc.
- Application Event Subscribers
- Application-level Exceptions
Use Cases are Application Services that wrap around the domain layer with. They are typically used via Use Case Request and Response value objects.
Product features like "confirm a payment", "create an order", etc. should be defined here.
Use Cases can also be seen as all the possible actions that an actor can perform in your service.
Application Layer Rules
The application layer SHOULD only know about the Domain Model layer and nothing about the Presentation or Infrastructure ones.
Use Case(s) SHOULD have a single purpose, therefore they MAY have just one method, e.g
Use Cases SHOULD always use value objects as method arguments and as return values.
If there is no returned value, it SHOULD be declared explicitly, e.g. as
Use Cases SHOULD only throw Application-layer exceptions, catching the expected ones from the Domain Model.
Multiple small Domain Model components/services SHOULD be used instead of having large Use Case classes.
Use Case(s) SHOULD NOT extend other Use Case nor depend on another Use Case: they SHOULD be independent. Services or Components of the Domain Model are the way to go for code modularization/reusability.
The Presentation Layer
This is the outermost layer (together with Infrastructure) and it is the window of the external clients to your application. It defines the interface of your application for the outside world.
It may consist of the following types of objects:
- HTTP controllers which handle HTTP requests and responses
- Forms or HTTP Request transformers (e.g. transforming requests to value objects to be used in the use cases)
- Input Validation
- Security (Authentication, Authorization, Roles)
- Response factories (from use cases, exception to response converters, etc.)
- CLI commands (console)
- Message Queue producers (outgoing messages).
- Message Queue consumers (incoming messages), consuming the Domain Events of external services.
Presentation Layer Rules
The Presentation layer SHOULD only know about the Application or DomainModel layers and nothing about the Infrastructure one.
HTTP Controllers are just a presentation layer of the Use Case(s).
HTTP Controllers SHOULD catch Application layer exceptions and resolve them into an HTTP response with a proper HTTP status code.
The Infrastructure Layer
The infrastructure layer is where we will implement the adapters of the interfaces of the other layers, most of them being typically in the Domain Model.
This layer mainly has everything related to external parties, e.g:
- DB adapters / repository interface implementations
- HTTP clients for external services
- Wrappers for vendor libraries or implementations of their interfaces
Infrastructure Layer Rules
This layer will mainly need code from the Domain Model, but will probably not use any of the other layers. To keep code clean, it's recommended to use only the Domain Model layer.
Domain Model repository / API client interfaces SHOULD be implemented here.
Code organization example of an onion-based Symfony app
This is an example structure similar to the one I use on my Symfony applications in my current company. It's not perfect but after some years using the Onion structure we've made some changes to make it more evident where to put everything.
/tests /src /Application /UseCase /CreateOrder CreateOrderRequest CreateOrderResponse CreateOrderUseCase /DomainModel /Orders /Service OrderCodeGenerator OrderStateManager /Factory OrderFactory /Entity OrderEntity /Event OrderCreatedEvent /Repository OrderRepositoryInterface /Presentation /Http /Authentication /Transformer RequestDataTransformer ExceptionTransformer ResponseFactory /EventSubscriber ExceptionSubscriber /Controller CreateOrderController /Cli /Command CreateOrderCommand /Mq /MessageProducer OrderCreatedMessageProducer /MessageConsumer ExternalPaymentMessageConsumer /Infrastructure /Repository /Mysql OrderRepository /PaymentProvider /PaymentProviderClient /FileStorage /FileSystemClient /S3Client /Support TokenGenerator DateFormatter /VendorLibrary /MyVendorLibraryExtension
After more than 10 years of experience as a PHP developer, this is the cleanest structure I've worked with, and believe me, PHP projects can get really messy very quickly.
One of the most important things in a team, an organization is that you have common software development guidelines where you explicitly define what's the target architecture of your different services, well-explained documentation that everyone understands and adopts in their daily work. It should be the reference in everyone's code reviews and it should be something you all agreed on as a group.
There also is this video from Eric Normand, where he explains the Onion Architecture more in detail in a very friendly way while driving his car 😎.