Creating a good architecture and structuring software is similar to cooking a good meal with a solid base of ingredients, but when it comes to cutting those onions, don't go shedding tears!
In this post I'll be giving an insight into Onion Architecture and how it can be used as a well structured alternative to traditional architecture models.
Ingredients of an Onion Architecture
The idea for an Onion Architecture was first introduced by Jeffrey Palermo and is a concept closely related to Hexagonal and Layered architectures. The difference being that it uses an alternate layered approach made up of four layers with the fourth acting as a lightweight or replaceable layer of components:
- Domain Model layer - domain entities - as the name suggests, this layer models real world entities as class representations.
- Domain Services layer - domain-defined processes - this layer ensures integrity of the domain model, through encapsulation of CRUD and data access operations.
- Application Services layer - application-specific logic - this is the use case layer, calling to the Domain Services layer using Domain Entities and the Infrastructure services. Also provides or acts as an interface to the outside world, which can be achieved through using the Outer UI layer.
- Outer layer - peripherals including UI, tests and databases/data-sources (infrastructure) - this is an infrastructure or peripheral layer, it acts as an interface to the outside world for interchangeable services or tools including databases and external web-services.
Onions have layers
The image below shows the layers and the overlap between each one.
The relationship between each layer is one way and follows the rule that outer layers can depend on inner layers but not the other way around. When we use Onion Architecture we're essentially applying the Dependency Inversion Principle in the form of a whole architecture instead of one or multiple classes.
The Domain Model, Domain Services and Application Services layers make up the Application Core. The application core is the structure that should stay the same and contains all the logic to run the outer layer. The outer layer is interchangeable through the use of interfaces, inverting the dependencies.
Cooking with onions
In a project we can structure our code into packages, these packages act as layers.
com.onion.model com.onion.domain com.onion.application com.onion.persistance
In a Scala project we can further enforce the relationship between each layer by scoping our classes to the packages themselves with
private[domain] final class MyClass() and defining the dependencies by using
dependsOn when creating projects in our
lazy val model = Project("onion-model", file("model")) .settings(Dependencies.model) .settings(PublishOnly(Set.empty)) lazy val domain = Project("onion-domain", file("domain")) .settings(Dependencies.domain) .settings(PublishOnly(Set.empty)) .dependsOn(model) lazy val application = Project("onion-application", file("application")) .settings(Dependencies.application) .settings(PublishOnly(Set.empty)) .dependsOn(domain) lazy val persistence = Project("onion-persistence", file("persistence")) .settings(Dependencies.persistence) .settings(PublishOnly(Set.empty)) .dependsOn(domain, application)
By enforcing these rules of dependencies, outer layers can interact with inner layers but those inner layers aren't dependent on the interacting outer layers.
Inner domain and application layers will define criteria through interfaces, in the outer layers implementations are created. Implementation behaviour is dependent on the signature its domain interface. A common example of this would be for database interaction - an interface is defined in the domain services layer. This isn't dependent on the type of database or the data-source meaning we won't break any application logic by moving from using a Postgres database to MongoDB as a source.
Arguing about onions
There are a number of pros and cons to Onion Architecture.
- Flexibility - the outer layer is completely decoupled from application logic making it easy to swap peripherals including databases, external services or testing tools.
- Prioritised/one way coupling - the most important code depends on nothing, whereas everything else depends on it.
- Clear dependency rules - enforces clear rules on where each mechanism sits and what it relies on.
- Interface soup - lots of interfaces for interaction with the core which can make navigating a project tougher.
- Core heavy - lots of logic and movement in the core, any changes made inside the core could affect behaviour outwards.
To summarize, Onion Architecture provides a well thought out approach to structuring software and is easy to visualize. While it doesn't solve problems of interfaces interfering with the readability of code and changes to inner layers can surface breaking changes to dependent outer layers. Onion Architecture is simple to implement and enforces good relationships with one way coupling between software components. The interchangeability and separation of peripherals ensures that core application processes aren't interfered with and can keep up with changing requirements, data sources and improving tools in the engineering world.
Next time you're building a project, why not throw in an onion!