In this article I look at my use of a Clean Code architecture with the modular monolith architecture covered in the first article. Like the first article this isn’t primer on Clean Code and modular monolith but is more about how I adapted the Clean Code 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 Code 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 Code architecture, plus a look at whether the time pressure of the project (which was about 5 weeks later) made me “break” any rules.
- The Clean Code 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 Code 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 Code 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 Code 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 Code architecture.
- Overall I think the Clean Code 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.
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 Code approach (also called the Hexagonal Architecture and Onion Architecture) is an development of the traditional “N-Layer” architecture (shortened to layered architecture). The Clean Code approach 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 Code 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 Code approach around the persistence (database) layer later in the article.
Links to more detailed information on clean code (unmodified)
- The Clean Architecture by Robert C. Martin (Uncle Bob)
- The Onion Architecture by Jeffrey Palermo
- An introductory-level article on the Clean Architecture
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).
I had used a simpler Clean Code architecture on a client’s project I worked on, so I had some ideas of what I would do. Clean Code 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 Code architecture again in a larger application.
To explain how Clean Code 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 Code 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 Code 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.
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 Code I should say that a layered architecture could also provide the layering that the Clean Code defined. It’s just that the Clean Code has some more rules, most of which are useful.
The main problem was fitting the EF Core DbContext into the Clean Code architecture. Clean Code 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 Code, but other than that it works fine. But other external services, such as an external email service, I would follow the Clean Code rules and add an interface in the inner (Domain) circle and register the service using DI.
Appling the Clean Code 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 Code 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 Code 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 Code approach has as big of effect on the structure that the modular monolith did (read the first article), but Clean Code 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 Code 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 Code 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 Code architecture than me, so you can give your experience too.