For learning and discussion purposes I’ve done up a series of short articles looking at the architecture of eShopOnWeb which is Microsofts sample ASP .NET Core reference application…
Even though I’d do a few things differently, eShopOnWeb is awesome for learning purposes. If I discuss alternative approaches to what it does, this is just my opinion. I’m not saying eShopOnWeb is ‘wrong’… context is everything.
On a high level eShopOnWeb uses clean architecture.
The main idea of clean architecture is that the solution is spilt into different projects (or layers) and that the domain project is at the centre with all other projects pointing into it. Other projects typically include an Infrastructure and UI project. Importantly these point into the domain project via interfaces not concrete dependencies.
Concrete dependencies on things like Entity Framework and Redis are defined in the Infrastructure project. Dependency injection is a fundamental concept in clean architecture and is used to inject these dependencies at runtime.
The diagram below (taken from the accompanying eBook for eShopOnWeb) shows a typical clean architecture setup…
The main proposed benefits of clean architecture is that because its heavily abstracted its easy to swap out implementations and also easy to test. I’ve definitely seen these benefits when I’ve used it BUT unfortunately these benefits come with huge cost in terms of extra complexity of the overall solution.
One thing in particular with clean architecture is that it’s so abstracted there’s extra cognitive work required to actually know where in the application something is actually done and how things ‘fit together’.
As clean architecture separates code by technical not functional concern it can often have really low functional cohesion and so code for any one use case is scattered all over the place. For example below shows the files (note.. some of these are needed by alternative architectures too) involved in the Get My Orders use case in eShopOnWeb. To get an end to end understanding of how this use case actually works I have to navigate through a lot of files and projects.
Click on the image for a larger view in a new window…
I find this kind of solution architecture very unsatisfying to work with as a developer and definitely notice myself using ‘Go to Implementation’ a lot when reviewing systems which use it.
Of course having so much separation of concerns also means debugging can be harder, onboarding can be harder, PRs can be larger as often a lot of files to change, tracking bugs back to check-ins can be harder, documenting the system can be harder and yes actually adding or changing features can IMHO also be much harder.
Vertical slice architecture as an alternative to clean architecture?
IMHO vertical slice architecture is a much more pragmatic and developer friendly macro-architecture.
With VSA we aim to limit abstractions and have high feature cohesion by placing concrete code which corresponds to any one feature or use case together. For example… in the below still from Derek Comartins Restructuring to a Vertical Slice Architecture video on YouTube he has restructured the eShopOnWeb code for the Get My Orders use case shown above so that it is all together…
It might not be one for the ‘purists’ among us but IMHO having all (or most) of the code for a particular use case together (doesn’t necessarily have to be in the same file) is much easier to work with than having to constantly navigate around layers of abstractions and multiple projects as is often the case with clean architecture.
Of course the above is just my opinion and using clean architecture is definitely not ‘wrong’… it’s extremely popular online and because context is everything some projects will absolutely benefit from the rigid structure it provides. In particular many architects feel it helps less senior developers into the ‘pit of success‘ which basically means its an architecture which makes it harder for them to do the wrong things due to how structured and prescriptive it is.
What do you like to use in your apps? Clean architecture, vertical slice architecture or something else?
We can see a nice example of the interface segregation principle in eShopOnWeb.
For its repository interfaces eShopOnWeb have two interfaces… IRepository and IReadRepository. This means that if a repository for any particular aggregate root only needs to query a DB it can just implement IReadRepository without having to worry about all the update related methods.
Click the image below for a larger view in a new tab…
Instead of adding many different test methods with different inputs set in the method body we have one method and pass in the inputs and expected output(s) for each test we want to run as params to the method (one per line). This means less test methods (so test logic easier to change) and that we can quickly see all inputs and expected outputs for a particular method together. In addition to this our results from each row of params will be grouped together.
Below is one example from eShopOnWeb which uses xUnit, but most test frameworks will support the concept. Click on the image for a larger view in a new tab…
In this particular test the query inside the specification is being tested.. for me this is a little bit too low level of a test. There’s very little I can refactor in the specification without changing its signature and thus requiring a change to the test so personally I would test this stuff but would do it through the context of its parent API or use case which appears to be…
CatalogViewModelService.cs -> GetCatalogItems and CachedCatalogViewModelService.cs -> GetCatalogItems
eShopOnWeb has a couple of its service classes (BasketService and OrderService) implement corresponding interfaces (IBasketService and IOrderService). These interfaces have no other implementations (even in test projects) so the value of including them in the architecture is highly questionable.
IBasketService and its references are shown below… click on the image for a larger view in a new tab. Single implementation interfaces in my opinion are just pointless. The whole point of using interfaces is that we can swap out multiple implementations without our calling code having to care about which actual concrete class is doing the end work.
When we have only one implementation of an interface, we are paying the cost of having the interface without getting any benefit. It’s easy to extract an interface when it’s needed so there’s no need IMHO to create interfaces ahead of time ‘just in case’.. and always remember YAGNI.
Using interfaces to aid testing
Testing is a special case whereby we might only have one implementation of an interface in our app itself but we have another implementation in our test project to facilitate plugging in mocked or stub test versions of our classes. In this case we still have multiple implementations… whether it’s a good idea to add a load of interfaces to our apps just for testing is debatable and is a question for a different day but we definitely are not in a single implementation scenario.
Using interfaces for DI
We don’t need interfaces to use dependency injection, this works…
Does it make sense to ever have multiple implementations of service classes?
Of course eShopOnWeb is just a reference architecture, it’s not a completed app, if it were perhaps there would be more implementations of IBasketService and IOrderService… BUT still the problem I see with this is that our services classes (and below) are basically our apps. The only thing we usually have above our service layer might be a thin HTTP or console app layer.
Our service classes are the main way into all the use cases and functionality of our app and contains all our business logic and usually already have their dependencies passed in. This means we don’t need multiple implementations for testing.
What about having multiple implementations in our app itself? Well are we really going to have multiple ways to implement our core use cases? For example below in the AddItemToBasket method we..
Check for existing basket
If there is none we create one
Then add the item to the basket
Then persist it to our datastore
The four steps needed to add an item to basket won’t change, if they do it’s a different use case and will be implemented by a different method.
IMHO the inclusion of the IBasketService and IOrderService interfaces just adds bloat and unnecessary complexity to the architecture.