What is Clean Architecture?
Clean Architecture is an architectural pattern designed to promote testability, maintainability, and a clear separation of concerns in .NET applications. Its primary strength lies in organizing code into distinct layers with well-defined responsibilities, ensuring that each layer can evolve independently.
One of the standout benefits of Clean Architecture, in my opinion, is its decoupling of the infrastructure layer. This means you can swap out external dependencies, like switching from Microsoft SQL Server to PostgreSQL, without altering your core business logic. This flexibility is invaluable for adapting to evolving project requirements or integrating with new technologies.
This is a simple schematic of a Clean Architecture application. In our case, this how each of our modules in our modular Monolith will look like.
If you look at the arrows in the diagram, you’ll notice that all dependencies point toward the Domain Layer. This is intentional and a fundamental principle of Clean Architecture. I’ll dive deeper into the significance of this in a future post about Domain-Driven Design.
Layers
Domain Layer
The Domain Layer is the heart of your application or module, representing the real-world concepts, rules, and processes that your software is designed to handle. It defines the core business logic and entities that reflect the problem domain, such as a Bike entity in a bike shop application or an Order entity in an e-commerce platform.
This layer models the real world with all its constraints and boundaries. For example, in a bike shop, a Bike entity might include properties like Brand, Model, Price, and StockAvailability, along with rules such as ensuring the price is always greater than zero. These business rules ensure the system behaves in a way that mirrors real-world requirements.
Everything else in the project, whether it’s the Application, Infrastructure, or Presentation layers, revolves around and interacts with the Domain Layer. By keeping this layer free from dependencies on other parts of the application, you ensure that your business logic remains pure, reusable, and easy to test.
Application Layer
The Application Layer is responsible for orchestrating the business logic of your application. It acts as the mediator between the Domain Layer and the other layers, such as Presentation and Infrastructure. This is where the different use cases of your application are implemented, ensuring that the core business rules in the Domain Layer remain decoupled from external concerns.
In our case, the Application Layer also follows the CQRS (Command and Query Responsibility Segregation) pattern, separating the logic for modifying data (commands) from the logic for reading data (queries). This separation enhances clarity, scalability, and maintainability, especially as the application grows in complexity.
Infrastructure Layer
The Infrastructure Layer is where all external dependencies and configurations for your application or module are managed. It handles integration with external services such as databases, file storage, email providers, message brokers (e.g., MassTransit setup), and other third-party APIs.
This layer serves as the bridge between your application and the outside world. For instance, if your application needs to switch from SQL Server to PostgreSQL, the Infrastructure Layer ensures this transition is seamless, without requiring changes to the Domain Layer or the core business logic in the Application Layer.
In some projects, the Infrastructure Layer is split into two distinct layers: an Infrastructure Layer and a dedicated Persistence Layer. This separation can be beneficial if there is significant configuration outside of database-related concerns, such as complex messaging setups, API clients, or caching mechanisms. While not mandatory, this approach helps maintain clarity by isolating database setup (e.g., Entity Framework or Dapper configurations) from other infrastructure concerns.
Presentation Layer
The Presentation Layer serves as the entry point for end users to interact with your application. It typically includes Web APIs, such as controllers or minimal API endpoints, designed to handle incoming requests and deliver responses.
When a request is received, the Presentation Layer directs it to the appropriate Command or Query in the Application Layer. This ensures that business logic is processed in the correct context and that the response returned to the end user is meaningful and accurate.
For example, in a shopping application, an API endpoint for placing an order might validate the user’s input, call a PlaceOrderCommand
in the Application Layer, and return a confirmation or an error message to the user.
This separation ensures that your presentation logic remains lightweight and focused on facilitating communication between the user and the deeper layers of the application.
Lets create a Clean Architecture Application/Module
Go and start Visual Studio. Create a new Blank Solution.
Give it a name. I call my Solution BikeShop.
Right-click on your Solution and go to Add > New Project. Add a new Class Library and call it BikeShop.Domain. At this point, your solution should look like this:
You can remove Class1.cs
Right-click your Solution again and add another Class Library. Name it BikeShop.Application
. Your solution should now look like this:
As mentioned earlier, the Domain Layer must not have any dependencies on other layers, but the Application Layer depends on the Domain Layer. So, right-click the Dependencies in your Application Layer and click Add Project Reference.
Select your Domain Project.
Next, add another Class Library by right-clicking the Solution, selecting Add > New Project, and again selecting a C# Class Library. Name it “BikeShop.Infrastructure”. Then, repeat this step to add a project called BikeShop.Presentation. Your solution should now look like this:
Lets setup the dependencies for Infrastructure and Presentation.
Right-click the dependencies in your Infrastructure project and add a Project Reference to Domain and Application.
For your Presentation you only need to introduce a dependency on the Application Layer.
Now, the very last step is to introduce your true Entry Point into the application. The Web API.
Right-click your Solution and add a new Project but this time it’s not a Class Library but an ASP.NET Core Web API.
Leave the next page as it is except for “Use Controllers”. Untick this because we will use Minimal APIs.
Your solution should look like this:
For now we will only setup a dependency from the API to the Application Layer because the API will have to invoke Commands or Queries living in the Application Project. For example:
app.MapGet("qs/societies/{id:guid}", async (Guid id, ISender sender, CancellationToken cancellationToken = default) =>
{
var result = await sender.Send(
new GetByIdQuery(id),
cancellationToken);
return result.Match(() => Results.Ok(result.Value), ApiResults.Problem);
})
As you can see, we need to create a GetByIdQuery that takes the id from the API request.
Alternatives
While Clean Architecture is a great approach, there are other alternatives like Vertical Slice Architecture, which is quite interesting, though I haven’t had the chance to explore it thoroughly yet.
TLDR
In this post, we set up a Clean Architecture application, creating separate layers for Domain, Application, Infrastructure, and Presentation. Each layer has clear responsibilities and dependencies, ensuring maintainability and testability. We also configured a Minimal API to interact with the Application Layer prepared for CQRS.