Question

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;
}
Was it helpful?

Solution

Your event definition is correct. An event can be transmitted over the wire, thus serialized and you don't want to send big,rich objects. Add the fact that the domain event is used by other bounded contexts which may have a very different definition of what Itinerary means or they wouldn't know about other domain objects. The point is not to couple bounded contexts (BC) but to communicate what happened using 'neutral' information as much as possible.

Primitives are great, followed by value objects especially if they mean the same all over the Domain.

What if some subscribers of CargoAssignedEvent need the itinerary's derivation to make decisicions?

It's impossible for the domain object to guess the needs of whatever subscriber. The domain object's duty is only to generate the event saying what happened. I think there is not a recipe on how to define an event. They have to represent a Domain past action, however choosing how to represent it in code depends on the developer and the app complexity. Using directly the involved agreggate root or rich entities is a technical hassle. What I prefer to use is either primitives (if possible) or a memento (DTO) which acts as initialization data for the domain object. But, ONLY if I consider that including a domain object is really needed by the event. I'm always trying to define an event without involving rich objects.

For this specific example, I think that the event handler can use a repository/service to get the objects it needs requiring only the itineraryId and some info contained in RouteCandidateDto.

EventStore adapters complicate things and I think it should be used ONLY for event versioning. The fact that a domain object was refactored didn't changed the event definition.

If the event contains only the domain object memento, then it's up the domain object to handle its versioning. Basically it's just adding a new constructor with MementoV2 as argument.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top