Modeling Domain Events
Let's see how to model domain events.
Join the DZone community and get the full member experience.
Join For FreeDomain Events in a Complex Domain
In one of my previous posts, we discussed Domain and Integration events. In this post, we will look at modeling domain events in order to simplify the application.
There are certain triggers that can lead to domain events.
- A command from a user/system
- The time like the End of the month or a duration
- Event causality — One event leads to other events
For a complex domain, these triggers can lead to a large number of domain events. For example, for an online food delivery service , there can be events related to:
- User Commands — AddFoodItem, RemoveFoodItem, UpdateQuantity, CancelOrder
- System Commands — PaymentCompleted, OrderShipped, OrderDelivered
- Duration Based — There could be hard limits within which an order needs to be prepared, picked and delivered to the customer.
- Event Causality Based — RestrauntOutOfService, ItemNotAvailable, DeliveryDelayed
As we can see, these are just some of the cases. Over time, this can lead to a lot of supporting classes aka Event Handlers. Additionally, the implementation can become overly complex based on figuring out the type of event at the application services layer. The above approach can also lead to a very large number of events per aggregate.
Simplifying the Domain Model
In order to mitigate the above issues, we had to simplify the Event domain model as well as and move away from having a large number of events per aggregate. If we look at the intent of having the events, we notice that these were related to transitions, which happen over the lifecycle of fulfilling an order. Since this ties in very well with the real world, we decided to model the events with the same perspective.
This led to modeling the DomainEvent as:
public abstract class DomainEvent implements Serializable{
private static final long serialVersionUID = 1L;
private @Getter String aggregateId;
private @Getter String parentId;
private @Getter Transitions transitions;
protected DomainEvent(String aggregateId, String parentId) {
setAggregateId(aggregateId);
setParentId(parentId);
}
/**
* @param aggregateId the aggregateId to set
*/
private void setAggregateId(String aggregateId) {
Objects.requireNonNull(aggregateId, "Aggregate Id can not be null");
this.aggregateId = aggregateId;
}
/**
* @param parentId the parentId to set
*/
private void setParentId(String parentId) {
this.parentId = parentId;
}
public void setTransitions(Transitions transitions) {
this.transitions = transitions;
}
}
Transitions is a set of state changes for an aggregate.
@Getter
@Setter
public class Transitions implements Serializable {
private List<Transitions> transitions;
}
The Transition class encapsulates the start and end states along with the quantity.
@Getter
@Setter
public class Transition implements Serializable {
private State startState;
private State endState;
private Double quantity;
private DateTime transitionDateTime;
}
The State is an enum with all the possible states.
Identifying Terminal Transitions
One of the key things was to identify terminal transitions for an event. For example, for an order with 2 food items, one of which is unavailable, the terminal events would be
- 1 item has been delivered to the customer
- 1 item has been canceled from the system, because of non-availability.
We can use the Transitions in the event to create a Digraph. The vertex for the graph is the State and the edge was the quantity. Terminal transitions were the states which had an out-degree of 0.
Resolving Event Handlers in Application Services
In order to resolve which EventHandler to invoke, we go through the terminal states and invoked applicable EventHandlers. A factory was used to resolve the handler based on the start and end state of the transition.
public class OrderShippedService extends OrderService{
private EventHandlerFactory eventHandlerFactory;
public void orderShipped(OrderEvent event){
List<Transition> terminalTransitions = getTerminalTransitions(event);
for(Transition transition: transitions){
EventHandler handler = eventHandlerFactory.getEventHandlerForTransition(transition.getStartState(),
transition.getEndState());
handler.handle(event);
}
}
//Other methods
}
What Goes in the Event Handler
An EventHandler would implement a particular use case. It would invoke appropriate application services to perform some action.
For example, when a order is shipped, a notification is sent to the customer. Additionally, the tracking service needs to start tracking the order.
public class OrderShippedEventHandler extends EventHandler<OrderEvent> {
private NotificationService notificationService;
private TrackingService trackingService;
public void handle(OrderEvent event){
notificationService.notify(event);
trackingService.track(event);
}
}
This approach greatly simplified the Domain Event model and event handlers. It also simplified resolving Event Handlers at the application services layer.
Further Reading
Building Microservices Through Event-Driven Architecture, Part 2: Domain Objects and Business Rules
Published at DZone with permission of Maneesh Chaturvedi. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments