Today, I am going to show you the best way learning Domain-Driven Design, Clean Architecture, CQRS, and Software Design Principles in practice. As you might know, there are many interesting books out there that help us gain valuable information about Domain-Driven Design, Clean Architecture, CQRS, and other topics. But nowadays, most developers don’t have enough time to read books or may not want to spend time reading books. Of course, I don’t admire this approach, but it is what it is. And in the era of AI, we want to learn everything fast. That’s why I think using tutorials and real-world examples from GitHub or other sources will help you reach your destination.
For that reason, I’m going to show you one of the best domain-driven design, CQRS, a clean architecture repository. And I hope you will like this repository. I have provided almost all possible best ways of implementing domain-driven design and clean architecture in this repo. You can just check the readme file here.
If you don’t like huge articles, jusat check my video where I explained this article’s content in more detail:
Of course, in GitHub, you may find a lot of interesting repositories which demonstrate Domain-driven design, clean architecture, CQRS, and other stuff. But fortunately, most of them are just garbage for me because they don’t demonstrate real-world examples, they are just pet projects.
But for me, the best way of implementing domain-driven design, is clean architecture should look like this. And that’s why I implemented this repository. So here we have a lot of interesting features.
✨ Features
*Basket Management:
*
- Create baskets with basket items.
- Underlying architecture for Add, update, and remove items.
- Underlying architecture for Calculate total amounts including tax and shipping.
Coupon System:
- Underlying architecture for Apply and remove discount coupons.
- Underlying architecture for Support for fixed and percentage-based discounts.
Clean Architecture:
- Separation of concerns with clearly defined layers.
- Independent and replaceable infrastructure.
Domain-Driven Design:
- Focus on business rules encapsulated in the domain layer.
- Events for tracking domain state changes.
- Tactical and Strategical DDD patterns: Ubiquitous Language,Bounded Context,Value Objects, Entities, Aggregates, Domain Events, Domain Services, Application services, Repositories, Factories,Modules.
Cross-Cutting Concerns:
- Logging: Centralized and consistent logging for debugging and monitoring.
- Validation: Reusable validation logic using FluentValidation to ensure data integrity.
- Exception Handling: Unique exception handling to provide meaningful error messages and prevent crashes.
I tried to implement almost all possible patterns from domain-driven design like ubiquitous language, bounded context, value objects, entities, aggregates, domain events, domain services, application services, repositories, factories, modules, and more.
The development process in our case starts from Event Storming.
Before diving into the details of the source code, please check the Event Storming Board. It will help you to understand why we implemented such type of features and Event Storming will help you to understand the core ubiquitous language used in our application.
Our Application was implemented using clean architecture and I applied best practices to demonstrate the usage of clean architecture in rich business applications.
Here is the simple explanation for the diagram:
First, we have a user. User sends request to the web API. We have the application layer which handles the API query using CQRS and it forwards, it acts like orchestrator between domain models. And we have domain models that we are retrieving, we are manipulating and domain models raises domain events.
These events will be caught by the event dispatcher. And for the event dispatching, we have events and event handlers. Our events will be caught by event handlers. And if you want to go outside of your bounded context, we have integration events that will help you to publish you event outside. For example, in our context, we have Apache Kafka implementation. You can push your event to the outside using Apache Kafka.
So as you see, we have approximately more than 10 Domain-Driven Design patterns here, but that’s not all. Of course, I have used other Design patterns, let’s say from the GoF Patterns. For example, we have the Greg Young’s CQRS here.
We have the Result pattern from the functional programming. We have a decorator, mediator, publisher-subscriber, strategy, template method, factory method, chain of responsibility, unit of work, and more. If you really want to see all these patterns in practice in real-world examples, this repository is one of the best to investigate and learn all these concepts using only one project.
So now let’s dive into the details of this project using Visual Studio. In our application, you might guess, we have two main folders. They are source(src) folder and tests folder.
In our source folder, we have clean architecture and the shared library functionality. And in tests folder, we have unit tests plus the test data.
Of course, I have just implemented unit testing here and I will add integration tests, but for now, I approximately implemented all possible unit test scenarios for this project. Let’s start from the source(src) folder. You should first try to understand the domain layer here.
In our domain design applications, you are implementing microservices using a domain-driven design. In this approach, you know, we have special naming rule. In our case, we have VOEConsulting.Flame.BasketContext.Domain. ‘VOE consulting’ is a company name. ‘Flame’ is the application name. ‘BasketContext’ is a bounded context name and the ‘Domain’ is the layer name.
Lets start from the domain layer.
We have Basket, Coupon and we have Common folders. In our basket, you will find all the entities, aggregates, value objects, events, and services related to the basket. And the same is applicable for the Coupon also. After reviewing our event storming, you will understand why we have basket and coupon implementation.
public sealed class Basket : AggregateRoot<Basket>
{
public IDictionary<Seller, (IList<BasketItem> Items, decimal ShippingAmountLeft)> BasketItems { get; private set; }
public decimal TaxPercentage { get; }
public decimal TotalAmount { get; private set; }
public Customer Customer { get; private set; }
public Id<Coupon>? CouponId { get; private set; } = null;
private Basket(decimal taxPercentage, Customer customer)
{
BasketItems = new Dictionary<Seller, (IList<BasketItem>, decimal)>();
TaxPercentage = taxPercentage.EnsurePositive();
TotalAmount = 0;
Customer = customer;
}
public void AddItem(BasketItem basketItem)
{
if (BasketItems.TryGetValue(basketItem.Seller, out (IList<BasketItem> Items, decimal ShippingAmountLeft) value))
{
value.Items.Add(basketItem);
}
else
{
BasketItems.Add(basketItem.Seller, (new List<BasketItem> { basketItem }, basketItem.Seller.ShippingLimit));
}
RaiseDomainEvent(new BasketItemAddedEvent(this.Id, basketItem));
}
public static Basket Create(decimal taxPercentage, Customer customer)
{
var basket = new Basket(taxPercentage, customer);
//basket.RaiseDomainEvent(new BasketCreatedEvent(basket.Id, customer.Id));
return basket;
}
}
We have implemented approximately all possible domain-driven design, strategical, and tactical patterns. For example, our Basket is an Aggregate Root. Our Coupon is an Aggregate Root also. Aggregate root is a type of entity that has possibilities related to events. You can add events, clear events, pop events ,and more.
Just go to the definition. I have implemented approximately all the base classes using by myself. I haven’t used any libraries here. So you see we have aggregate root, it is inherited from the entity and it has this sort of functionalities.
public abstract class AggregateRoot<TModel> : Entity<TModel>, IAggregateRoot
where TModel : IAuditableEntity
{
private readonly IList<IDomainEvent> _domainEvents = new List<IDomainEvent>();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
public IReadOnlyCollection<IDomainEvent> PopDomainEvents()
{
var events = _domainEvents.ToList();
ClearEvents();
return events;
}
public void ClearEvents()
{
_domainEvents.Clear();
}
protected void RaiseDomainEvent(IDomainEvent domainEvent)
{
domainEvent.EnsureNonNull();
_domainEvents.Add(domainEvent);
}
}
The best way of exploring this repository is you don’t need to implement everything from scratch. I will have series of tutorials which help you to understand this project in more detail.
This is just an overview of the application which help you to understand the high level implementation of domain driven design, clean architecture, CQRS and the software development best practices. So just go to VOEConsulting.Flame.Common.Domain. And here we have all sorts of contracts, abstract classes for our application.
All sorts of aggregates inherited from Aggregate Root. If you have entity, your entity should inherit from the abstract entity class. If you have value object, your value object should inherit from the abstract value object class. Also, I have mentioned that if you want to use records, there is no need to implement such type of class because the records will handle all the equality component functionality.
In our domain layer, I’m throwing exceptions, but catching these sort of mechanisms using result pattern in a domain layer is also completely okay. I mean, you don’t need to throw exceptions. You can just easily wrap them to the result pattern and send it to the application layer. This is also completely okay, but I have used the exception-throwing mechanism here.
We validate entities, aggregates, and value object through these extension methods. For example, in our basket, we have, for example, ensure positive.
private Basket(decimal taxPercentage, Customer customer)
{
BasketItems = new Dictionary<Seller, (IList<BasketItem>, decimal)>();
TaxPercentage = taxPercentage.EnsurePositive();
TotalAmount = 0;
Customer = customer;
}
Ensure positive is a checking mechanism will help us to check if the given value is positive or not. So I have approximately all sorts of extension methods for all sorts of scenarios, but in the future, I may add or remove some of them.
Long story short, in our VOEConsulting.Flame.BasketContext.Domain, we have domain oriented logic,on the other hand, in VOEConsulting.Flame.Common.Domain we have shared domain layer features.
I will create a separate NuGet package that will help you to just easily install and use these features. But right now, you can just by yourself create your own private NuGet repository and use it.
The next layer in our application is going to be our Application Layer. The responsibility of our application layer is to handle commands and queries using CQRS. In our Web API, we have request mechanism and we are requesting some data and we are forwarding it to our application layer using CQRS.As you might guess, I have future based mechanism and it looks like a vertical slice architecture. For instance, we have create basket functionality. It has Command, Command handler and validator; like all in one.
The command handling mechanism is really easy because we have common handler base, which we have wrapped all the repeated steps into one abstract class and using template method pattern. You can just inherit and rewrite the exact **ExecuteAsync **method. So every time when we are handling a request, we have a special core handling mechanism.
I don’t have any validation code here because I have cross-cutting concerns in our behaviors on the application. So you see we have exception handling pipeline behavior and this pipeline behavior will help you to handle the exception stuff. And we have logging and we have validation.
We have a domain model orchestration like aggregate/entity orchestration in our application layer.
Of course, the domain layer throws exceptions, but I mentioned, exception handling pipeline behavior will catch it and will provide the value for you result.
The domain events will help us to notify the changes about our aggregate inside our bounded context. But if you want to go outside of our bounded context, you should map it to the integrated event and raise the integrated event.
public sealed class BasketCreatedIntegrationEvent : IntegrationEvent
{
public BasketCreatedIntegrationEvent(Id<Basket> basketId, Guid customerId)
: base(basketId)
{
CustomerId = customerId;
}
public BasketCreatedIntegrationEvent() { }
public Guid CustomerId { get; set; }
}
You have orchestration mechanism because in one domain model you are storing a part of business related to use case in another domain model you are storing another part of business so we need to orchestrate them to get one use case that’s why we are using application layer and it is responsible for publishing our domain events and of course.
So integration events will be handled in our infrastructure layer because we are going outside of our bounded context. For example, we need to publish an event to message queue and we will implement the exact implementation of message queue in our infrastructure layer. And we have repository interfaces. Why I haven’t put these interfaces to domain layer because I don’t think that it is the best way of implementing repository interfaces in our case. Why? Because in my domain layer, I’m not using these interfaces. This is a bit design decision. If you have the exact implementation for your interfaces, if you are actually using these interfaces in the domain layer, you should put them to domain layer. If you are not using them in your domain layer, it would be better to move them to the upper layer like the application layer. In my case, I put them into our application layer.
Now lets talk about our infrastructure layer. The responsibility of infrastructure is to work with outside elements. If you have mail sending, you have working with GraphQL, Apache Kafka, external services, databases, you should put all these mechanisms’ implementation inside your infrastructure. For example, I have put my interfaces to the application layer, but this interfaces implementation will be stored in our infrastructure layer.
So,as you see, I have persistence, have all sort of entity framework configurations, I have all sort of entities here, profiles for mapping and the exact unit of work implementation. I have DB context here. I have all sorts of repository implementations.
In our infrastructure, we are handling all the exact implementations related to the outside world.
And the higher one is our API layer.
We have simple **BasketController **here. The responsibility of basket controller is actually getting the request data from the user and forward it to the lower layers. In my case, for example, we have create basket.
[ApiController]
[Route("api/[controller]")]
public class BasketController(ISender sender, ILogger<BasketController> logger) : BaseController(logger)
{
private readonly ISender _sender = sender;
// GET api/basket/{id}
[HttpGet("{id}")]
public async Task<IActionResult> GetBasket(Guid id)
{
var result = await _sender.Send(new GetBasketQuery(id));
return result.IsSuccess ? Ok(result) : HandleError(result.Error);
}
// POST api/basket
[HttpPost]
public async Task<IActionResult> CreateBasket([FromBody] CreateBasketCommand command)
{
var result = await _sender.Send(command);
if (result.IsSuccess)
return CreatedAtAction(nameof(GetBasket), new { id = result.Value }, result.Value);
return HandleError(result.Error);
}
}
basket creation is a command, not a query. That’s why we have a create basket command. Using ISender’s send, we are forwarding it to the application layer, actually. And the combination of application + infrastructure will handle all this stuff for us. I have implemented base controller that will help us easily work with controllers. I don’t have too much logic because you should not put logic to your API layer if they are not related to the API layer. For example, if I need to wrap some data from the lower layer to the bad request ,I don’t now, to the 404, 400, 500, of course, I should interact with API layer. Otherwise, you should not add any logic to this API layer. So just check this controller. I will add more features. Of course, we haven’t implemented all possible endpoints for basket, for coupon, but I will add them one by one. This application will help you to understand domain driven design, clean architecture, and of the best way of implementing software. And of course, we have tests. How it is possible to write application without tests. I have almost implemented
all possible best ways of using unit tests in this application. Starting from the next tutorials, I will create a series of articles to explain almost all possible parts of this application. I will start from the unit tests and using these implementations, you can easily understand and adapt it to your current projects.
So that’s what I have implemented and I hope you will love this repository. Just go to Tural Suleymani’s GitHub and in my git, you will find the real domain-driven design, CQRS and clean architecture.
🌟 If you find this repository helpful or interesting, please don’t forget to give it a star! It really helps and motivates me to keep improving and sharing more projects. Thank you! 🌟
See you in our next tutorials, bye.
Want to dive deeper?
Regularly, I share my senior-level expertise on my TuralSuleymaniTech youtube channel, breaking down complex topics like .NET, Microservices, Apache Kafka, Javascript, Software Design, Node.js, and more into easy-to-understand explanations. Join us and level up your skills!
Top comments (0)