Domain-Driven Design example
This project is an example of a .NET implementation of a medical prescription model using the "Domain-Driven Design" (DDD) approach and the "Command and Query Responsibility Segregation" (CQRS) architectural pattern.
Points of interest
This project has been created to test an implementation in .NET of the core concepts of DDD (entities, value objects, domain events, ...) and CQRS (commands, queries, command and query handlers, ...) on the basis of a concrete model.
The final goal of the project is to develop, for personal use, the main components of a message-based architecture that combines the benefits of the Hexagonal Architecture, CQRS and DDD, namely :
The scalability of the system is important, but is not the main concern. I want to develop a solution that combines the above-mentioned benefits, but that is accessible to small development teams and that can be deployed in different types of environment. For this reason, only a light version of CQRS (no data projection, one data store) has been considered and no messaging framework (like Apache Kafka or NServiceBus) has been used.
The main components are developed from technologies commonly used by .NET developers :
Architecture
The architecture of the project is based on the Hexagonal Architecture. The main concepts of the Hexagonal Architecture are clearly explained in the article "Hexagonal Architecture, there are always two sides to every story". Applying the architectural pattern CQRS in this architecture offers many benefits :
Packaging and deployment
In recent years, microservices architecture has become very popular in the IT community : this architecture has been popularized by the big tech companies. The main reason for adopting this architecture is organizational : each service can be developed in its own technology by its own team and can be deployed separately. This architecture, presented as modular, is often contrasted with monolithic architecture, considered as non-modular. However, it is possible to modularize a monolithic application by distributing the application code in different libraries and by assembling them into a monolith for deployment. Such an application is called a modular monolith. As explained in the article "Why should you build a (modular) monolith first?", an adequately produced modular monolith can be a good step that can be more or less transformed into a microservice solution tomorrow if needed. This project has been designed to support such type of application.
Message handling
In a message-based application, 3 types of messages can be handled :
Commands and queries are usually handled synchronously and events asynchronously. Some events called "domain events" capture an occurrence of something that happened in the domain and that is considered important by business experts. These events generally register that the state of the application (more precisely the state of an aggregate) has changed and they occur during the processing of a command. It is important to transactionally record this change and the associated event(s). A simple way to do this is to record them in the same database. This is the way taken by the project : events and aggregates are saved in the same database.
Domain events are mainly used to decouple the different parts (bounded contexts) of the application. Each part of the application can be interested in events that happened in the other parts. When you set up a delivery mechanism of events, you must take into account various concerns :
From a consumer's perspective, we all want a scalable and easy-to-use mechanism that guarantees that events will be delivered exactly once and in order, but it is not technically possible : guarantee an ordered delivery of all events makes the solution non-scalable. However, we can divide the whole stream of events (from a bounded context to another) into smaller streams and ensure the order of events within these streams (under normal conditions). We can also ensure that events are processed exactly once in a bounded context : it is possible by storing the current position in the stream in the database associated with the consuming context.
In this project, each bounded context is responsible for recording its own events in its own database as well as associating them with a stream type (usually an aggregate type) and a stream identifier (usually an aggregate identifier). Each context is also responsible for registering its own subscriptions to event streams, reading the streams to which it has subscribed, handling the associated events and updating the current position within these streams. When an error occurs during event handling and no “immediate” retry strategy has been defined via a decorator (command handlers can be easily decorated), all events with the same stream type and stream ID as the event causing the error are excluded from event handling. This exclusion can be temporary if the error is considered transient and a delayed retry strategy has been defined.
Model
The model considered is a simplified model reflecting the life cycle of a medical prescription and, in particular, of a pharmaceutical prescription. This life cycle can be summarized by the following diagram.
The current model only takes into account the use cases related to the prescriber : the creation and revocation of a prescription.
Command Model
On the write side, a rich domain model has been used to encapsulate the business logic and to control the creation of domain events. This model represents the business model. It incorporates both behavior and data, and is composed of entities and value objects.
The domain modeling has been focused on two important aspects :
Therefore, the domain model has been implemented as an object model without public setters and by hiding some information (like database identifiers for the value objects). Entity inheritance and value object inheritance has been used and some value types like enumerations has been implemented as value objects.
Two options has been considered to map the domain objects to the database tables :
By comparing the purity/complexity ratios of the two options, the first option is preferred to map the domain model to the database. In the branch "NHibernate", some minor changes have been made to the domain model, such as adding protected constructors or private setters.
The interactions between the main components on the write side can be represented as follows :
Query Model
As mentioned above, the command and query data stores are not differentiated but the architecture on the query side is simplified (as shown on the following diagram). The query side is composed of simple Data Transfer Objects mapped to the database by using the Micro ORM Dapper.
Organization of code
The libraries are distributed by component (bounded context) :
The application layer can be tested by using the "DDD.HealthcareDelivery.IntegrationTests" project.
Cross-cutting concerns
The decorator pattern is especially useful in CQRS to handle cross-cutting concerns such as logging or error handling. Command or query handlers (small interfaces) can be easily decorated. You will find some examples of decorators in the "DDD.Core.Polly" and "DDD.Core" projects.
Exception chaining (or exception wrapping) has been used. Each abstraction has its own set of exceptions :
The Domain and Application layers have their own base exception class (respectively DomainException and ApplicationException). These classes defines a property IsTransient indicating whether the exception is transient.