In this article I look at my use of a clean architecture with the modular monolith architecture covered in the first article. Like the first article this isn’t primer on Clean Architecture and modular monolith but is more about how I adapted the Clean Architecture to provide the vertical separation of the features in the modular monolith application.
- My experience of using modular monolith and DDD architectures.
- My experience of using the Clean Architecture with a Modular Monolith (this article).
Like the first article I’m going to give you my impression of the good and bad parts of the Clean Architecture, plus a look at whether the time pressure of the project (which was about 5 weeks later) made me “break” any rules.
See my new series on building modular monoliths where I take my experience and come up with a better approach to building modular monoliths using the .NET architecture.
- Evolving modular monoliths: 1. An architecture for .NET
- Evolving modular monoliths: 2. Breaking up your app into multiple solutions
- Evolving modular monoliths: 3. Sharing data between bounded contexts
TL;DR – summary
- The Clean Architecture is like the traditional layered architecture, but with a series of rules that improve the layering.
- I build an application using ASP.NET Core and EF Core using the Clean Architecture with the modular monolith approach. After this application was finished, I analysed how each approach had worked under time pressure.
- I had used the Clean Architecture once before on a client’s project, but not with the modular monolith approach.
- While the modular monolith approach had the biggest effect on the application’s structure without the Clean Architecture layers the code would not be so good.
- I give you my views of the good, bad and possible “cracks under time pressure” for the Clean Architecture.
- Overall I think the Clean Architecture adds some useful rules to the traditional layered architecture, but I had to break one of those rules you make it work with EF Core.
A summary of the Clean Architecture
NOTE: I don’t describe the modular monolith in this article because I did that in the first article. Here is a link to the modular monolith intro in the first article.
The Clean Architecture approach (also called the Hexagonal Architecture and Onion Architecture) is an development of the traditional “N-Layer” architecture (shortened to layered architecture). The Clean Architectureapproach talks about “onion layers” wrapped around each other and has the following main rules:
- The business classes (typically the classes mapped to a database) are in the inner-most layer of the “onion”.
- The inner-most layer of the onion should not have no significant external code e.g., NuGet packages, added to it. This is designed to keep the business logic as clean and simple as possible.
- Only the outer layer can access anything outside of the code. That means:
- The code that users access, e.g. ASP.NET Core, is in the outer layer
- Any external services, like the database, email sending etc. is in the outer layer.
- Code in inner layers can’t reference any outer layers.
The combination of rules 3 and 4 could cause lots of issues as lower layers will need to access external services. This is handled by adding interfaces to the inner-most layer of the onion and registering the external services using dependency injection (DI).
The figure below shows how I applied the Clean Architecture to my application, with is an e-commerce web site selling book, called the Book App.
NOTE: I detail the modification that I make to Clean Architecture approach around the persistence (database) layer later in the article.
Links to more detailed information on Clean Architecture (unmodified)
- The Clean Architecture by Robert C. Martin (Uncle Bob)
- The Onion Architecture by Jeffrey Palermo
- An introductory-level article on the Clean Architecture
Setting the scene – the application and the time pressure
In 2020 I was updating my book “Entity Framework Core in Action” I build an ASP.NET Core application that sells books called Book App. In the early chapters is very simple, as I am describing the basics of EF Core, but in the last section I build a much more complex Book App that I progressively performance tuned, starting with 700 books, then 100,000 books and finally ½ million books. For the Book App to perform well it through three significant enhancement stages. Here is example of Book App features and display with four different ways to display the books to compare their performance.
At the same time, I was falling behind on getting the book finished. I had planned to finish all the chapters by the end of November 2020 when EF Core 5 was due out. But I only started the enhanced Book App in August 2020 so with 6 chapters to write I was NOT going to finish the book in November. So, the push was on to get things done! (In the end I finished writing the book just before Christmas 2020).
My experience of using Clean Architecture with a Modular Monolith
I had used a simpler Clean Architecture on a client’s project I worked on, so I had some ideas of what I would do. Clean Architecture was useful, but its just another layered architecture with more rules and I had to break one of its key rules to make it work with EF Core. Overall I think I would use my modified Clean Architecture again in a larger application.
A. What was good about Clean Architecture?
To explain how Clean Architecture helps we need to talk about the main architecture – the modular monolith goals. The modular monolith focuses on features (Kamil Grzybek called them modules). One way to work would have one project per feature, but that has some problems.
- The project would be more complex, as it has everything inside it.
- You could end up with duplicating some code.
The Separation of Concerns (SoC) principal says breaking up a feature parts that focus on one part of the feature is a better way to go. So, the combination of modular monolith and using layers provides a better solution. The figure below shows two modular monolith features running vertically, and the five Clean Architecture layers running horizontally. The figure has a lot in it, but it’s there to show:
- Reduce complexity: A feature can be split up into projects spread across the Clean Architecture layers, thus making the feature easier to understand, test and refactor.
- Removing duplication: Breaking up the features into layer stops duplication – feature 1 and 2 share the Domain and Persistence layers.
The importance of the Service Layer in the Clean Architecture layers
Many years ago, I was introduced to the concept of the Service Layer. There are many definitions of the Service Layer (try this definition), but for me it’s a layer that knows about the lower / inner layer data structures and knows about the front-end data structures and it can adapt between the two structures (see LHS of the diagram above). So, the Service Layer isolates lower layers from having to know how the front-end works.
For me a Service Layer is a very important level.
- It holds all the business logic or database accessed that the front-end needs, normally providing as services. This makes it much easier to unit test these services.
- It takes on the job of adapting data to / from the front end. This means this layer that has to care about the two different data structures.
NOTE: Some of my libraries, like EfCore.GenericServices and EfCore.GenericBizRunner are designed to work as a Service Layer type service i.e., both libraries adapts between the lower / inner layer data structures to the front-end data structures.
Thus, the infrastructure layer, which is just below the Service Layer, contains for services that are still working in the entity class view. In the Book App these projects contained code to seed the database, handle logging and providing event handling. While services in the Service Layer worked with both lower / inner layer data structures and front-end data structures.
To end the “good” part of Clean Architecture I should say that a layered architecture could also provide the layering that the Clean Architecture defined. It’s just that the v has some more rules, most of which are useful.
B. Clean Architecture – what was bad?
The main problem was fitting the EF Core DbContext into the Clean Architecture. Clean Architecture says that the database should be on the outer ring, with interfaces for the access. The problem is there is no simple interface that you can use for the application’s DbContext. Even if you using a repository pattern (which I don’t, and here is why) then you have a problem that the application’s DbContext has to be define deep in the onion.
My solution was to put the EF Core right after to the inner circle (name Domain) holding the entity classes – I called that layer persistence, as that’s what DDD calls it. That breaks one of the key rules of the Clean Architecture, but other than that it works fine. But other external services, such as an external email service, I would follow the Clean Architecture rules and add an interface in the inner (Domain) circle and register the service using DI.
Clean Architecture – how did it fair under time pressure?
Appling the Clean Architecture and Modular Monolith architectures together took a little more time to think thought (I covered this in this section of the first article), but the end result was very good (I explain that in this section of the first article). The Clean Architecture layers broke a modular monolith feature into different parts, thus making the feature easier to understand and removing duplicate code.
The one small part of the clean architecture approach I didn’t like, but I stuck to, is that the Domain layer shouldn’t have any significant external packages, for instance a NuGet library, added to it. Overall, I applaud this rule as it keeps the Domain entities clean, but it did mean I had to do more work when configuring the EF Core code, e.g. I couldn’t use EF Core’s [Owned] attribute on entity classes. In a larger application I might break that rule.
So, I didn’t break any Clean Architecture rules because of the time pressure. The only rules I changed were make it work with EF Core, but I might break the “Domain layer and no significant external packages” in the future.
I don’t think the Clean Architecture approach has as big of effect on the structure that the modular monolith did (read the first article), but Clean Architecture certainly added to the structure by breaking modular monolith features into smaller, focused projects. The combination of the two approaches gave a really good structure.
My question is: does the Clean Architecture provide good improvements over a traditional layered architecture, especially as I had to break one of its key rules to work with EF Core? My answer is that using the Clean Architecture approach has made me a bit more aware of how I organise my layers, for instance I now have an infrastructure layer that I didn’t have before, and I appreciate that.
Please feel free to comment on what I have written about. I’m sure there are lots of people who have more experience with the Clean Architecture than me, so you can give your experience too.
That’s an interesting article. When I start reflecting on better architecting my apps, I came across the vertical slice architecture which changed drastically the quality of our apps.
Here are some useful links :
Vertical Slice Architecture – Jimmy Bogard
Clean Architecture with ASP.NET Core 3.0 – Jason Taylor – NDC Sydney 2019
CQRS and MediatR in ASP.NET Core
Why use MediatR? 3 reasons why and 1 reason not
The mediator pattern is so useful !
Simple mediator implementation in .NET
Yep, I have seen these approaches and Kamil Grzybek uses a vertical slice in his excellent series on the Modular Monolith. I haven’t gone that way because a) layers tend to stop duplicate code, and b) I built the library EfCore.GenericServices that is directly accesses the database which means I don’t need MediatR.
Have a look at my new series on modular monolith where I use the layer approach, but it the vertical slice architecture works for you then that great.
Great article. I was hoping to see benefit of using CA in which “will give you a power of switching different ORMs” . On the other hand, CA + modular monolith seems a pretty handy solution
As I said its hard to turn EF Core into an interface. @ardalis does add an repository interface for his example (see this GitHub repo) but even then he has to put EF Core in the infrastructure layer. I’m not a fan of the repository pattern as it can get very big as your app grows.
PS. Have you seen my newer series on a modular monolith design for .NET apps? I delve more into the layers and Domain Driven Design bounded context approach.