2
votes

Intro

This question is about DDD and Event Sourcing where entities within an Aggregate other than the Aggregate Root have event-generating behaviour.

Example

What follows is an example of the situation I describe, where I'm sure I want to encapsulate some logic inside other entities within the Aggregate. This may involve suspension of disbelief with respect to the actual example and whether it is a good model or not. :)

I'm trying to model a DeliveryRun Aggregate Root (AR), which is the trip a vehicle makes to perform a delivery. Before it departs, it must have an up to date DeliveryManifest. The "up-to-dateness" of it suggests to me that the DeliveryManifest be an entity within the DeliveryRun consistency boundary defined by the AR.

Okay so far.

I'm using an Event Sourcing approach for this - the approach as taught by Greg Young and implemented in the Regalo library. This means the AR (DeliveryRun) need not actually have any entities if there is no behaviour for them (e.g. a SalesOrder may not have SalesOrderLines, because it records events such as ItemsAdded/ItemsRemoved instead).

However, there is to be some logic around the DeliveryManifest. Specifically, once the manifest has first been requested, when items are added to the delivery, a new version of the manifest needs to be created. This means we can ensure drivers don't depart without the most up-to-date manifest available.

If I were to encapsulate the logic inside the DeliveryManifest object (which won't be serialised and stored; we're using Event Sourcing and it's not the AR), how do I capture events?

Options I'm considering

  • Should the events be generated by the DeliveryManifest entity, but saved against the DeliveryRun itself (which would then need to know how to replay those events into the DeliveryManifest when loaded from the event store)?

  • Should there be no DeliveryManifest (except perhaps as a data structure) and all the logic/events be implemented directly by the DeliveryRun?

  • Should the DeliveryManifest be it' own AR and make sure the DeliveryRun is told of the current manifest's ID? Since that takes the manifest object outside the consistency boundary of the DeliveryRun, I would need to build some event handling to subscribe to changes in the DeliveryRun that are relevant to the manifest so it can be updated/invalidated etc accordingly.

  • Implement a different style for capturing the events similar to Udi's DomainEvents pattern. This means changing the Regalo library, though I think it could be made to support both patterns fairly easily. This would allow all events generated by all entities within the aggregate to be captured so they can be saved against the AR. I'd need to think of a solution for loading/replaying though...

2
Care to explain how software is going to prevent a delivery truckdriver from departing with an out-dated manifest? You may want to mitigate the risk of it ever happening, but not prevent it. Also, you may want a book of record of what actually happened (even it the manifest was wrong, on that specific run, you want to know which manifest was taken along) versus what was planned to happen. - Yves Reynhout
Upon reflection, it seems to me that the life cycle of a manifest and a run are different, but happen to overlap at some point in time. The manifest might change many times before the actually delivery starts. When the delivery run is planned, you might indicate which manifest should be taken along. Maybe people should stop making changes to manifests once they're scheduled to be delivered (maybe not or maybe there's a point in time before the actual delivery is going to take place up to which people can change the manifest). Clearly, I lack domain knowledge ... - Yves Reynhout
Okay so I expected to get comments like that on a question involving DDD, but really my question was meant to be based on the assumption that I have modelled it correctly, and therefore how might I implement the encapsulation of logic in entities other than the AR? If I am literally writing C# code in a behaviour method on my AR, could it delegate to other entity classes that encapsulate logic? If so, how might one capture the events those entities generate? If they don't generate any themselves, where do the events come from? - Neil Barnwell

2 Answers

2
votes

I would avoid making DeliveryManifest another Aggregate Root unless it's a consistency boundary.

Many samples don't deal with this problem. It seems like it should be the responsibility of the aggregate root to collect events from entities inside it, and to distribute them to the correct entities for loading later on, which seems to be your option 1.

Option 2 is also perfectly good if there's no behaviour associated with the DeliveryManifest.

0
votes

The mechanical answer ... where you can dream up lots of variations. Basically, you'll have to decide who is going to collect all those events: either the root (shown here), or each entity (approach not shown here) separately. Technically you have lots of options to implement the observation behavior (think Rx, hand-coded mediator etc) shown below. I surfaced most of the infrastructure code into the entities (missing abstractions here).

public class DeliveryRun {
  Dictionary<Type, Action<object>> _handlers = new Dictionary<Type, Action<object>>();
  List<object> _events = new List<object>();

  DeliveryManifest _manifest;

  public DeliverRun() {
    Register<DeliveryManifestAssigned>(When);
    Register<DeliveryManifestChanged>(When);
  }

  public void AssignManifest(...) {
    Apply(new DeliveryManifestAssigned(...));
  }

  public void ChangeManifest(...) {
    _manifest.Change(...);
  }

  public void Initialize(IEnumerable<object> events) {
    foreach(var @event in events) Play(@event);
  }

  internal void NotifyOf(object @event) {
    Apply(@event);
  }

  void Register<T>(Action<T> handler) {
    _handlers.Add(typeof(T), @event => handler((T)@event));
  }

  void Apply(object @event) {
    Play(@event);
    Record(@event);
  }

  void Play(object @event) {
    Action<object> handler;
    if(_handlers.TryGet(@event.GetType(), out handler)) {
      handler(@event); //dispatches to those When methods
    }
  }

  void Record(object @event) {
    _events.Add(@event);
  }

  void When(DeliveryManifestAssigned @event) {
    _manifest = new DeliveryManifest(this);
    _manifest.Initialize(@event);
  }

  void When(DeliverManifestChanged @event) {
    _manifest.Initialize(@event);
  }
}

public class DeliveryManifest {
  Dictionary<Type, Action<object>> _handlers = new Dictionary<Type, Action<object>>();
  DeliveryRun _run;

  public DeliveryManifest(DeliveryRun run) {
    _run = run;
    Register<DeliveryManifestAssigned>(When);
    Register<DeliveryManifestChanged>(When);
  }

  public void Initialize(object @event) {
    Play(@event);
  }

  public void Change(...) {
    Apply(new DeliveryManifestChanged(...));
  }

  void Register<T>(Action<T> handler) {
    _handlers.Add(typeof(T), @event => handler((T)@event));
  }

  void Play(object @event) {
    Action<object> handler;
    if(_handlers.TryGet(@event.GetType(), out handler)) {
      handler(@event); //dispatches to those When methods
    }
  }

  void Apply(object @event) {
    _run.NotifyOf(@event);
  }

  void When(DeliveryManifestAssigned @event) {
    //...  
  }

  void When(DeliveryManifestChanged @event) {
    //...  
  }
}

P.S. I coded this "out of my head", please forgive me for the compilation errors.