It's a fact, there is no Single Silver Bullet for every solution, but we can come damn close. Having the right architecture sets a solid foundation for any solution. When I say "architecture", I mean structuring the solution in a specific strategic way; with the aim to have the components (or layers) loosely coupled and tightly cohesive to the maximum extent possible keeping in mind the feasibility of the trade-off.
One can implement different design patterns on top of an architecture pattern to standardise the usability of the code for a specific solution. Building solutions depends on the applicable technology and the following questions come to mind: Is it available? How often does it change? Is it supported?
I want to emphasise one particular question - How often does it [the technology] change? Baring in mind the complexity of the solution; there's a big chance of changing technology. In this context, having the words "change" and "complex" in one sentence raises warning signs that change is eventually inevitable for the dependent technology.
Comfortably embrace change - be ready
We must strive to have maintainable and extensible components in our solution. This is where the term "Onion Architecture" comes into play. It emphasizes separation of concerns throughout the system.
I must stress that this type of architecture is best suited for complex behaviour and long-lived business applications. It may not be feasible for small websites.
Onion Architecture emphasizes the use of Contracts (Interfaces) and forces externalization of the implementation thereof. This gives the ability to freely interchange different implementations (that may be dependent on different types of technology) in our system.
Traditional Layered Architecture
The problem with the "traditional" N-tier architectural layered design in this context is that the upper layers are dependent on the layers beneath as well as the possible "shared" layer (Infrastructure), making the very bottom layer highly dependable.
In this case, any changes in the "Storage" layer can easily cause a ripple-effect to the other layers.
Problems with Traditional Architecture
- Very easy for developers, over time to put more and more business logic in the UI layer
- Counter-productive to build your application on top of a specific technology that is sure to change over time
- Logic is easily scattered all over, locating code becomes a major effort.
- Developers over time struggle to determine where code should go… DAL? BLL? Utilities?
- Business logic has many tentacles extending from it (directly and indirectly)
- Library explosion: Makes it easy take a dependency without putting much thought into it, and now it’s littered all over the code base
The illustration on the right shows a similar architecture: Both the User Interface (Presentation-layer) & Tests on the outside reference the Business Logic that references the Data Access Layer that talks to the persistant storage.
Note that references work from the outside towards the centre.
Even with an ACL (represented by the white spaces), bending the layout in a circle we discover something significant: The Storage-layer (database) is in the centre.
The center of your application is not the database, it's the use cases of your application.
— Fanie Reynders (@FanieReynders) May 16, 2012
Looking back at one of my previous Twitter quotes, I remembered my philosophical moment: The database should not be the centre concern of our application. Why should the business case dictate & worry about where and how the data will be stored? Business [logic] should never be dependent on the database.
Bring on the Onion
Derived from the onion's circular layered structure, Onion Architecture isn't a new concept. It's been around for a while now. It allows the use of pluggable components that is the external implementations of the internal contracts & models.
It relies heavily on the Dependency Inversion Principle (DIP) for configuring bindings and injecting the instances toward the implementation (on the edges) of the application during run-time.
Starting from the outside, we have a Dependency Resolution layer responsible for providing instances to contracts using the DIP like Inversion of Control (IoC) and references all the layers.
- The UI layer represents the front-end logic of the application.
- Infrastructure is externalized and provides the implementations responsible for invoking web-services, data access, logging, mapping etc. Only technology-specific code (non-business) belongs here.
- The Tests layer houses all tests in the solution.
- All externally exposed functionality (like REST & WCF) is implemented in the API Service layer.
- The UI-, Infrastructure-, Tests- and API Service layer respectfully references the contracts and models in the Core layer (represented by the white line).
The Business Logic, or the Core layer; is in the centre. Here you will find everything unique to the business: Domain model, validation rules and business workflows. This layer cannot reference any external libraries and contains no technology-specific code.
Key aspects of Onion Architecture:
- The application is built around an independent object model
- Inner layers define interfaces. Outer layers implement interfaces
- Direction of coupling is toward the centre
- All application core code can be compiled and run separate from infrastructure
- True loose coupling between layers/components
- Limit re-write necessity over time, business value-adding logic is entirely self-contained
- Application becomes very portable – next version of .NET or an entirely new language/platform
- Business logic has no dependency tentacles (aside from your platform dependencies)
- Architecture is more easily sustained over time, developers know exactly where components go, a lot of guesswork is removed
- Infrastructure is free to use any data access library or external web services to do its work
- UI and Data Access “layers” become much smaller, deal strictly with technology-related code
- No more need for Common/Shared/Utilities project
- Compiler enforces dependencies boundaries, impossible for Core to reference Infrastructure
That's all folks! Hopefully someone will find this interesting and especially useful somewhere in their solution implementations.
Remember to draw the system using concentric circles which can only take dependency on something provided in an inner layer. Externalise all technology-related code and push complexity as far out as possible.
Till next time...