The Onion Architecture is an Architectural Pattern that enables maintainable and evolutionary enterprise systems. It is intended for use at a Solution/System level.
The primary proposition of this architecture is good coupling.
What is coupling ? It is the level of dependency of one thing upon another. The higher the coupling, the lower the ability to change and evolve the system.
Dependencies should be inward and never outward. Code should depend only on the same layer or layers more central to itself.
Business Logic behaviour is declared as contracts with the use of interfaces in a Object-Oriented context.
Separation of concerns
Each layer bounds together concepts that will have a similar rate of change.
The very centre of the Model, this layer can have dependencies only on itself. It represents the Entities of the Business and the Behaviour of these Entities.
This layer contains the implementation of the behaviour contracts defined in the Model layer.
This layer is the bridge between external infrastructure and the domain layers. The domain layers often need information or functionality in order to complete business functionality, however they should not directly depend on these. Instead, the application layer needs to depend on the the contracts defined in the Domain Services layer.
Databases, Messaging systems, Notification systems, User Interface, etc.
Things to Consider
- This approach is biased towards Object Oriented Programming (OOP). However it’s principles can still be applied in a wider sense.
In the future I’d like to explore and write about similar architectures applied to other programming paradigms such as Functional Programming.
- One of the primary objectives of this architecture is to increase maintainability. To achieve this level of maintainability, there is significant work involved in firstly setting up the structure, and secondly maintaining it along the life of the system. Implementation of features may be slower, because there are multiple layers to get through. That’s why Jeffery Palermo recommends it for Enterprise Systems, and not smaller systems non-complex systems.
- I find this pattern to help greatly with Test Driven Development (TDD). I often find it easier to drive out business logic code through tests than I do integrated code. With the Onion Model, I can write my tests against my business domain models and domain services interfaces with ease as they all sit in one place with minimal dependencies and no outward dependencies.
- Dependency Injection is a necessary evil with this architecture. It causes us to rely heavily on something quite external that binds the entire application together and allows it to function at run-time. That being said, it’s not a big deal and it does not outweigh the pros.
- Interfaces define behaviour contracts and stand as foundations amongst the layers. They are the key ingredient in this recipe.
I wanted to compliment this article with some code. In my implementation, I intend to demonstrate some of the key layers of this architecture and how they work together. See example repository here.
Note — The following is my interpretation of this Architecture Pattern and may not be as intended by it’s publishers. In saying that, I have seen this version survive production systems in the wild proving it’s maintainability tenet.
Note, I have chosen to call the most centre layer “Core” rather than Domain Models — a personal preference.
The Problem Domain
We will use the business domain of a Ride Sharing/Taxi Booking Application. It’s quite straight-forward, just think of Uber or Lyft. There is a Rider — someone who needs to travel from point A to point B, and a Driver — the car driver who will pick-up and drop-off the rider in their vehicle.
One of the first flows includes a Price estimate. The rider selects their destination, then are presented with an estimated price for their trip. Trip estimation is a business use-case, and it’s the one I’ve selected for our implementation. Figure 2 below outlines the domain within the application structure.
Estimating the fare is a core business use case. The business would not functional well if it could not give it’s customers proper pricing. Hence this behaviour shall be declared in the most central layer in the interface IRiderFareCalculator.
Our fare calculation depends on external services such as routing information and fare models. Interfaces for these are defined in the Domain Services layer — IFareRepostory and IRouteService. RiderFareCalculator is implemented in this layer also, and it depends on the fare repository and route service interfaces declared in the same layer. Note that with this approach, we do not depend on the external service, rather the external service depends on our declared contracts.
In the Application layer, the FareRepository is able to retrieve data from external sources and transform it into meaningful Business Entities.
You will see the the Domain Model/Core layer is referenced across multiple layers, and that’s fine, to a certain degree. We are also able to write Unit Tests for our business logic whilst not coupling our tests to implementation either.
Have a look through the code. Once again here is the link https://github.com/shivendraodean/architecture-onion.
Thanks for reading!