It's natural to use domain objects as fields in domain event based on the definition "Domain Event is a representation of something happened in the domain".
When using event sourcing, the domain events are persistent. So if they use domain objects as their fields, the domain objects are persistent as well. This dilutes the advantages gained by adopting CQRS & Event sourcing, making the domain objects more difficult to change and evolve.
Consider a CQRS version of Eric Evans' dddsample, the user story is:
Given a cargo has been registered
And I request possible routes for the cargo
And some routes are shown
When I pick up a candidate
Then the cargo is assigned to the route
public class Cargo { // This is an aggregate
private TrackingId trackingId;
private RouteSpecification routeSpecification;
public void assignToRoute(final Itinerary itinerary) {
Delivery delivery = Delivery.derivedFrom(routeSpecification, itinerary);
apply(new CargoAssignedEvent(this.trackingId,
itinerary, delivery.routingStatus()));//sending the domain event
}
}
public class Itinerary { //This is a value object
private List<Leg> legs;
}
public class Leg { //Another value object
private VoyageNumber voyageNumber;
private UnLocode loadLocation;
private UnLocode unloadLocation;
private Date loadTime;
private Date unloadTime;
}
public class CargoAssignedEvent { // This is a domain event
private final String trackingId;
private final RouteCandidateDto route; //DTO form of itinerary containing a List of LegDto s
private final String routingStatus;
public CargoAssignedEvent(TrackingId trackingId, Itinerary itinerary,
RoutingStatus routingStatus) {
this.trackingId = trackingId.getValue(); //transform to primitive
this.route = toRoute(itinerary); ////transform to DTO
this.routingStatus = routingStatus.getCode(); //transform to primitive
}
......
}
As you can see, I use DTO as fields of DomainEvent to seperate Domain models(Itinerary, RoutingStatus) from event persistence concerns. But this may cause some inconvinience and troubles on the event handler side. What if some subscribers of CargoAssignedEvent need the itinerary's derivation to make decisicions? Then I have to map the RouteCandidateDto to Itinerary.
An potential solution is using domain objects as fields but introduce some adapters in the event store. Use the adapters to map the domain objects and dto when loading or saving events.
Am I doing it right? Any idea is appreciated.
UPDATE
The itinerary is perhaps an special case. It is regarded as a whole value, so I cannot split this value object into a group of smaller domain events like CargoLegEvent(TrackingId, Leg). Consider the delivery case, Delivery is another important value object in the cargo domain wich is much richer than the Itinerary:
/**
* The actual transportation of the cargo, as opposed to
* the customer requirement (RouteSpecification) and the plan (Itinerary).
*
*/
public class Delivery {//value object
private TransportStatus transportStatus;
private Location lastKnownLocation;
private Voyage currentVoyage;
private boolean misdirected;
private Date eta;
private HandlingActivity nextExpectedActivity;
private boolean isUnloadedAtDestination;
private RoutingStatus routingStatus;
private Date calculatedAt;
private HandlingEvent lastEvent;
.....rich behavior omitted
}
The delivery indicates the current states of the cargo, it is recalculated once a new handling event of the cargo is registered or the route specification is changed:
//non-cqrs style of cargo
public void specifyNewRoute(final RouteSpecification routeSpecification) {
this.routeSpecification = routeSpecification;
// Handling consistency within the Cargo aggregate synchronously
this.delivery = delivery.updateOnRouting(this.routeSpecification, this.itinerary);
}
/**
* Updates all aspects of the cargo aggregate status based on the current
* route specification, itinerary and handling of the cargo. <p/> When
* either of those three changes, i.e. when a new route is specified for the
* cargo, the cargo is assigned to a route or when the cargo is handled, the
* status must be re-calculated. <p/> {@link RouteSpecification} and
* {@link Itinerary} are both inside the Cargo aggregate, so changes to them
* cause the status to be updated <b>synchronously</b>, but changes to the
* delivery history (when a cargo is handled) cause the status update to
* happen <b>asynchronously</b> since {@link HandlingEvent} is in a
* different aggregate.
*/
public void deriveDeliveryProgress(final HandlingHistory handlingHistory) {
this.delivery = Delivery.derivedFrom(routeSpecification(), itinerary(),
handlingHistory);
}
It came to my mind that I need a CargoDeliveryUpdatedEvent at first, like:
//cqrs style of cargo
public void deriveDeliveryProgress(final HandlingHistory handlingHistory) {
apply(new CargoDeliveryUpdatedEvent(
this.trackingId, delivery.derivedFrom(routeSpecification(),
itinerary(), handlingHistory);
}
class CargoDeliveryUpdatedEvent {
private String trackingId;
private DeliveryDto delivery;//DTO ?
}
But finally I found out that I could use smaller events which could reveal the intention better, like:
//cqrs style of cargo
public void deriveDeliveryProgress(final HandlingHistory handlingHistory) {
final Delivery delivery = Delivery.derivedFrom(
routeSpecification(), itinerary(), handlingHistory);
apply(new CargoRoutingStatusRecalculatedEvent(this.trackingId,
delivery.routingStatus());
apply(new CargoTransportStatusRecalculatedEvent(this.trackingId,
delivery.routingStatus());
....sends events telling other aspects of the cargo
}
Since the events are smaller and more specific, DeliveryDto and the mapper(domain object <--> DTO) it entails is not needed any more:
class CargoRoutingStatusRecalculatedEvent{
private String trackingId;
private String routingStatus;
}
class CargoTransportStatusRecalculatedEvent{
private String trackingId;
private String transportStatus;
}