Answering 3 Classic Questions

Answering 3 Classic Questions #

Hello, I am Ou Chuangxin.

Up to this session, we have completed the content of our Basics and Advanced courses. Throughout this process, I have been paying attention to the questions you have raised. Before we officially start the Practical section, I would like to explain three typical questions, and I hope you can also think about them and apply what you have learned. This will be helpful for our learning in the Practical section.

Question 1: Regarding the topic of dividing a domain into Core Domains, Generic Domains, Supporting Domains, as well as the relationships between Subdomains and Bounded Contexts, are there quantifiable criteria for boundary division?

In [Lesson 02], I mentioned that as the domain is continuously divided, it is gradually subdivided into different subdomains. This process is actually the process of narrowing down the problem scope.

To quote the reader “Password 123456,” he believes, “For domain problems, it can be understood as continuously dividing a problem until it becomes a small problem that we are familiar with and can handle quickly. Then, prioritize the handling of these small problems.”

This understanding is very accurate. After the domain is subdivided to a certain extent, we can conduct Event Storming for this subdomain, define the Bounded Contexts for this subdomain, and establish a Domain Model. Then, we can proceed with Microservice design based on the Domain Model.

Although DDD does not explicitly state the relationship between subdomains and bounded contexts, personally, I think the division of subdomains is a relatively coarse-grained boundary division of the domain. It does not consider the domain objects within the subdomain, the relationships between objects, and the structure. The division of subdomains is often based on business stages or functional module boundaries, aiming to facilitate the analysis of business scenarios using Event Storming within a relatively small problem space.

On the other hand, Bounded Contexts are essentially subdomains. Bounded Contexts are defined within a specific subdomain using Event Storming. They reflect a detailed design process. This process defines a Domain Model and clarifies the relationships between Domain Objects and their dependencies. With a Domain Model, you can directly proceed with Microservice design.

As for Core Domains, Generic Domains, and Supporting Domains, the main purpose of dividing these three different types of subdomains is to differentiate the priorities of business domains and determine the IT strategic investments. We will allocate important resources to Core Domains to ensure that they are well utilized. Each enterprise may have differences in their Core Domains due to different business models or strategic directions. Therefore, do not approach different enterprises’ Core Domains with a fixed mindset.

Core Domains, Generic Domains, and Supporting Domains are all business domains, but they have different levels of importance and functional attributes. The DDD design methods and processes used are the same.

Currently, there are no quantifiable criteria for domain and bounded context divisions. It mainly depends on the expertise of domain experts and continuous deliberation and analysis with the project team during the Event Storming process. Do not expect to establish a perfect Domain Model for complex business scenarios with just one iteration. Many times, a Domain Model also requires multiple iterations to take shape and needs continuous evolution. However, if the boundaries of the Domain Model designed using DDD and the boundaries of the Microservice cohesive are very clear, the evolution process will relatively be simpler, and the time cost will also be lower.

Question 2: Regarding Aggregate design, why does the Domain layer depend on Inversion of Dependency (DIP)?

Aggregates are mainly responsible for implementing core business logic. They contain many domain objects, and these domain objects need to be managed uniformly through the aggregate root to ensure data consistency. In domain-driven design, we use two important design patterns: the Factory pattern and the Repository pattern. If you are interested in learning more, I recommend reading Chapter 11 and Chapter 12 of the book “Implementing Domain-Driven Design”.

So why do we introduce the Factory pattern?

This is because some aggregates may contain a large number of entities and value objects, and we need to ensure that both the aggregate root and all dependent objects are created simultaneously. If we construct them all through the aggregate root, it will be very complex. Therefore, we can use the Factory pattern to encapsulate the creation process of complex objects. However, not all objects need to be constructed using a factory. If the construction process is not complex and only involves constructing a single object, a simple constructor method is sufficient.

Why do we introduce the Repository pattern? While answering this question, I will also address the issue of dependency inversion.

In the traditional four-layer DDD architecture, all layers depend on the infrastructure layer. What is the downside of this approach? If the application logic relies too heavily on the infrastructure layer, code related to resources in the infrastructure layer may seep into the application logic. Moreover, technology components are updated frequently, and once changes are made to the infrastructure components and the code of the infrastructure components is brought into the application logic, it can have a fatal impact on the upper-level application logic.

To decouple the application logic and the infrastructure resources, a repository layer is added between the infrastructure layer and the upper-level application logic. Each aggregate corresponds to a repository, and the repositories are responsible for the persistence of data within the aggregates. The application logic inside the aggregate accesses the infrastructure resources through an interface, and the repositories are implemented in the infrastructure layer. This way, the implementation logic of the application logic and the infrastructure resources are separated. If there are changes in the infrastructure resources, only the repository implementation needs to be replaced, which will not have a significant impact on the application logic. This achieves decoupling of the application logic and the infrastructure resources, and thus realizes dependency inversion.

Regarding some principles in the aggregate design process, most business scenarios can identify the aggregate root, establish aggregates, define boundaries, and build domain models through event storming. However, in some scenarios such as data calculation, statistics, and batch processing, all entities are independent and unrelated, and it is not possible to identify an aggregate root or establish a domain model. But their business relationships are very close and exhibit high cohesion. In these cases, we can consider such scenarios as a single aggregate, disregarding the design methods related to aggregate root, and other design methods related to DDD layered architecture can still be used.

For some business scenarios, if the complexity is not high and applying DDD design will bring unnecessary trouble, such as adding complexity, some principles can be broken. It is alright to adopt traditional approaches, as long as the boundaries of the domain model and the logical boundaries of aggregates within microservices are clear. This way, the evolution of microservices will not be too complicated.

Question 3: How can data consistency be ensured between the publisher and subscriber in the domain events that use message-driven asynchronous mechanism? Is it necessary to use an event bus for domain events between aggregates within a microservice?

In the design of domain events, to decouple microservices, eventual consistency is used for data between microservices. Because the publisher does not care about whether the data is delivered or whether the subscriber processes it properly after publishing the message on the message bus, some tech professionals may be concerned about the data consistency between the publisher and subscriber.

For business scenarios that require high data consistency, we have relevant design considerations. Both the sender and the receiver of the event data must store the events in a database. In addition to saving the business data, the sender writes the event to a local database before publishing it to the message middleware. The receiver writes the received event to a local database before processing it. Regular reconciliation of event data between the publisher and subscriber can be performed to identify any inconsistent data. If there are exceptions or inconsistencies, a scheduled program can be initiated to resend the events, and manual intervention can be carried out if necessary.

As for the question about the event bus, since the logic within a microservice resides in one process and there is only one backend database, the transactions within a microservice are easier to control compared to transactions between microservices. When handling domain events within a microservice, introducing an event bus will increase the complexity of development. Thus, whether to introduce an event bus depends on your own considerations.

In my personal opinion, if your scenario does not involve data inconsistency between aggregates, you may not need to use an event bus. Additionally, coordination between aggregates can also be achieved through application services.