IMHO overengineering is a curse on our sector. I see it all the time, it often takes the form of layers and layers of abstraction or the choosing of flavour of the month architectures such as microservices because ‘hey, that’s what Netflix does!!!’…
There’s some really good ideas in the Reddit thread below on how to avoid overengineering…
The approach I like to use which helps me to keep a grasp of complexity is to imagine or pretend it was my money being spent when choosing the architecture. I can then ask myself honestly… for example do we really need angular?, do we really need microservices? do we really need some additional layer of abstraction to cover some ‘just in case’ scenario?
It’s easy to spend someone else’s money… but if it was really my money being spent would I consider these approaches wasteful? Is there simpler ways to implement things which will still meet the requirements?
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