Domain events: design and implementation

Domain events: design and implementation

Use domain events to explicitly implement side effects of changes within your domain. In other words, and using DDD terminology, use domain events to explicitly implement side effects across multiple aggregates. Optionally, for better scalability and less impact in database locks, use eventual consistency between aggregates within the same domain.

What is a domain event?

An event is something that has happened in the past.

A domain event is, something that happened in the domain that you want other parts of the same domain (in-process) to be aware of. The notified parts usually react somehow to the events.

An important benefit of domain events is that side effects can be expressed explicitly.

For example, if you're just using Entity Framework and there has to be a reaction to some event, you would probably code whatever you need close to what triggers the event. So the rule gets coupled, implicitly, to the code, and you have to look into the code to, hopefully, realize the rule is implemented there.

On the other hand, using domain events makes the concept explicit, because there is a DomainEvent and at least one DomainEventHandler involved.

For example, in the eShopOnContainers application, when an order is created, the user becomes a buyer, so an OrderStartedDomainEvent is raised and handled in the ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler, so the underlying concept is evident.

In short, domain events help you to express, explicitly, the domain rules, based in the ubiquitous普遍存在的 language provided by the domain experts. Domain events also enable a better separation of concerns among classes within the same domain.

It's important to ensure that, just like a database transaction, either all the operations related to a domain event finish successfully or none of them do.

Domain events are similar to messaging-style events, with one important difference. With real messaging, message queuing, message brokers, or a service bus using AMQP, a message is always sent asynchronously and communicated across processes and machines. This is useful for integrating multiple Bounded Contexts, microservices, or even different applications. However, with domain events, you want to raise an event from the domain operation you are currently running, but you want any side effects to occur within the same domain.

The domain events and their side effects (the actions triggered afterwards that are managed by event handlers) should occur almost immediately, usually in-process, and within the same domain. Thus, domain events could be synchronous or asynchronous. Integration events, however, should always be asynchronous.

Domain events versus integration events

Semantically, domain and integration events are the same thing: notifications about something that just happened. However, their implementation must be different. Domain events are just messages pushed to a domain event dispatcher, which could be implemented as an in-memory mediator based on an IoC container or any other method.

On the other hand, the purpose of integration events is to propagate committed transactions and updates to additional subsystems, whether they are other microservices, Bounded Contexts or even external applications. Hence, they should occur only if the entity is successfully persisted, otherwise it's as if the entire operation never happened.

As mentioned before, integration events must be based on asynchronous communication between multiple microservices (other Bounded Contexts) or even external systems/applications.

Thus, the event bus interface needs some infrastructure that allows inter-process and distributed communication between potentially remote services. It can be based on a commercial service bus, queues, a shared database used as a mailbox, or any other distributed and ideally push based messaging system.

因为integration event是跨domain的,但是又不共享event类,所以event需要分别在发布者和订阅者处,写一份相同的代码,为了事件日志的序列化

事件的发布者C:workspaceGitHubMicrosofteShopOnContainerssrcServicesOrderingOrdering.APIApplicationIntegrationEventsEventsOrderStartedIntegrationEvent.cs

事件的订阅者C:workspaceGitHubMicrosofteShopOnContainerssrcServicesBasketBasket.APIIntegrationEventsEventsOrderStartedIntegrationEvent.cs

并且事件的订阅者basket处还需要有事件的handler,C:workspaceGitHubMicrosofteShopOnContainerssrcServicesBasketBasket.APIIntegrationEventsEventHandlingOrderStartedIntegrationEventHandler.cs

并且basket有订阅的代码   eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();

如果integration event跨越多个domain,那么每一个订阅的domain,都需要订阅这个事件,并且分别实现事件的handler,同时每一个domain都会有相同的event class,这是namespace不同

Domain events as a preferred way to trigger side effects across multiple aggregates within the same domain

If executing a command related to one aggregate instance requires additional domain rules to be run on one or more additional aggregates, you should design and implement those side effects to be triggered by domain events.

As shown in Figure 7-14, and as one of the most important use cases, a domain event should be used to propagate传播 state changes across multiple aggregates within the same domain model.

一个domain event会在同一个domain下的多个aggregates中进行传播

Figure 7-14. Domain events to enforce consistency between multiple aggregates within the same domain

Figure 7-14 shows how consistency between aggregates is achieved by domain events. When the user initiates an order, the Order Aggregate sends an OrderStarted domain event. The OrderStarted domain event is handled by the Buyer Aggregate to create a Buyer object in the ordering microservice, based on the original user info from the identity microservice (with information provided in the CreateOrder command).

Alternately, you can have the aggregate root subscribed for events raised by members of its aggregates (child entities). For instance, each OrderItem child entity can raise an event when the item price is higher than a specific amount, or when the product item amount is too high. The aggregate root can then receive those events and perform a global calculation or aggregation.

It is important to understand that this event-based communication is not implemented directly within the aggregates; you need to implement domain event handlers.

Handling the domain events is an application concern. The domain model layer should only focus on the domain logic—things that a domain expert would understand, not application infrastructure like handlers and side-effect persistence actions using repositories. Therefore, the application layer level is where you should have domain event handlers triggering actions when a domain event is raised.

Domain events can also be used to trigger any number of application actions, and what is more important, must be open to increase that number in the future in a decoupled way. For instance, when the order is started, you might want to publish a domain event to propagate that info to other aggregates or even to raise application actions like notifications.

The key point is the open number of actions to be executed when a domain event occurs. Eventually, the actions and rules in the domain and application will grow. The complexity or number of side-effect actions when something happens will grow, but if your code were coupled with "glue" (that is, creating specific objects with new), then every time you needed to add a new action you would also need to change working and tested code.

This change could result in new bugs and this approach also goes against the Open/Closed principle from SOLID. Not only that, the original class that was orchestrating the operations would grow and grow, which goes against the Single Responsibility Principle (SRP).

On the other hand, if you use domain events, you can create a fine-grained and decoupled implementation by segregating responsibilities using this approach:

  1. Send a command (for example, CreateOrder).
  2. Receive the command in a command handler.
    • Execute a single aggregate's transaction.
    • (Optional) Raise domain events for side effects (for example, OrderStartedDomainEvent).
  3. Handle domain events (within the current process) that will execute an open number of side effects in multiple aggregates or application actions. For example:
    • Verify or create buyer and payment method.
    • Create and send a related integration event to the event bus to propagate states across microservices or trigger external actions like sending an email to the buyer.
    • Handle other side effects.

As shown in Figure 7-15, starting from the same domain event, you can handle multiple actions related to other aggregates in the domain or additional application actions you need to perform across microservices connecting with integration events and the event bus.

Figure 7-15. Handling multiple actions per domain

There can be several handlers for the same domain event in the Application Layer, one handler can solve consistency between aggregates and another handler can publish an integration event, so other microservices can do something with it. The event handlers are typically in the application layer, because you will use infrastructure objects like repositories or an application API for the microservice's behavior. In that sense, event handlers are similar to command handlers, so both are part of the application layer. The important difference is that a command should be processed only once. A domain event could be processed zero or n times, because it can be received by multiple receivers or event handlers with a different purpose for each handler.

Having an open number of handlers per domain event allows you to add as many domain rules as needed, without affecting current code. For instance, implementing the following business rule might be as easy as adding a few event handlers (or even just one):

When the total amount purchased by a customer in the store, across any number of orders, exceeds $6,000, apply a 10% off discount to every new order and notify the customer with an email about that discount for future orders.

https://github.com/dotnet-architecture/eShopOnContainers/tree/dev/src/Services/Ordering

C:workspaceGitHubMicrosofteShopOnContainerssrcServicesOrderingOrdering.DomainEventsOrderStartedDomainEvent.cs
C:workspaceGitHubMicrosofteShopOnContainerssrcServicesOrderingOrdering.APIApplicationDomainEventHandlersOrderStartedEventValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler.cs

C:workspaceGitHubMicrosofteShopOnContainerssrcServicesOrderingOrdering.APIInfrastructureAutofacModulesMediatorModule.cs

  // Register the DomainEventHandler classes (they implement INotificationHandler<>) in assembly holding the Domain Events
            builder.RegisterAssemblyTypes(typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler).GetTypeInfo().Assembly)
                .AsClosedTypesOf(typeof(INotificationHandler<>));

A better domain events pattern

Domain events are one of the final patterns needed to create a fully encapsulated domain model – one that fully enforces a consistency boundary and invariants.

The need for domain events comes from a desire to inject services into domain models.

What we really want is to create an encapsulated domain model, but decouple ourselves from potential side effects and isolate those explicitly.

原文地址:https://www.cnblogs.com/chucklu/p/13086011.html