Handling Non-Aggregate Root Events for Child Entities

How should events be applied to child Entities when the Aggregate Root ultimately handles incoming commands? For example, constructing an Entity that is within the Aggregate Root boundary. Where do you call the constructor so that the constructor obeys the invariants being protected by the Entity?

Looking at our First Pop Coffee sample project, a RoastDay Aggregate Root has a concept of Batch which contains a sequence of Batches that are planned roasts of a specific quantity of a green coffee bean that the roaster has in inventory. These Batch Entities make up the RoastSequence for a RoastDay

public class RoastDay : AggregateRootEntity
{
    public RoastDayId RoastDayId { get; private set; }
    public RoastDate RoastDate { get; private set; }
    public List<Batch> RoastSequence { get; private set; }
 	...

The Batch Entity has its own invariants to protect, we have some simple ones for demonstration but there could be more:

public class Batch
{ 
    public BatchId Id { get; private set; }
    public string BeanName { get; private set; }
    public BatchQuantity Quantity { get; private set; }

    public Batch(Guid batchId, string beanName, decimal quantity)
    {
        if (batchId == Guid.Empty) throw new ArgumentNullException(nameof(batchId), $"{nameof(batchId)} cannot be an empty Guid");
        if (quantity <= 0.00m) throw new ArgumentException("quantity cannot be less than 0");

        Id = new BatchId(batchId);
        BeanName = beanName;
        Quantity = new BatchQuantity(quantity);
    }
}

Notice this is not structured like our event-sourced Aggregate Root, it’s a traditional Entity that has a constructor used by the Aggregate Root to add new Batch Entities to the RoastSequence for the RoastDay. The Aggregate Root is the only Entity that’s available to the outside world. To the outside world, it’s always the Aggregate Root who says that any invariants have failed. When using an event-sourced Aggregate Root, how do we make sure that commands sent to the Aggregate Root obey the invariants protected by the child Entity?

We’ve discussed the structure of the event-sourced Aggregate Root, but to reiterate, the event-sourced Aggregate Root has methods that are called by Command Handlers that 1) protect invariants, failing if guards are not met and 2) emit events signifying that the Aggregate Root has changed state.

From Greg Young’s SimpleCQRS project:

public abstract class AggregateRoot
{
	...

    protected void ApplyChange(Event @event)
    {
        ApplyChange(@event, true);
    }

    // push atomic aggregate changes to local history for further processing
    private void ApplyChange(Event @event, bool isNew)
    {
        this.AsDynamic().Apply(@event);
        if(isNew) _changes.Add(@event);
    }
}

A command-handling method on our RoastDay Aggregate Root for adding a new Batch Entity to its List<Batch> RoastSequence collection might look like this:

public void AddBatchToRoastSequence(Guid batchId, string beanName, decimal batchQuantity)
{
    ApplyChange(new BatchAddedToRoastSequence(batchId, beanName, batchQuantity));
}

But where do we actually create the new Batch using its constructor, which protects its invariants? We are not supposed to throw exceptions in our Apply(BatchAddedToRoastSequence @event) method, as we cannot have the constructor fail due to some invariant not being met after we emit the event.

In other words, we shouldn’t do this:

public class RoastDay : AggregateRootEntity
{
    public RoastDayId RoastDayId { get; private set; }
    public RoastDate RoastDate { get; private set; }
    public List<Batch> RoastSequence { get; private set; }

    private RoastDay()
    {
        ...

        Register<BatchAddedToRoastSequence>(@event =>
        {
            RoastSequence.Add(new Batch(@event.BatchId,
                                        @event.BeanName,
                                        @event.BatchQuantity));
        });
    }
	...

Because this would be checking the invariants on Batch after the BatchAddedToRoastSequence was emitted, potentially causing the event to fail and events shouldn’t fail. This is especially true when replaying. Sure, you could argue that the event never makes it to our event store and we’re ok. This spec passes:

[Fact]
public void Should_throw_if_invalid_quantity()
{
    var batchId = Guid.NewGuid();
    new CommandScenarioFor<RoastDay>(RoastDay.Factory)
        .Given(new RoastSequenceAddedToRoastDay())
        .When(sut => sut.AddBatchToRoastSequence(batchId, "Guatemala Acatenango Gesha Lot 2", 0.00m))
        .Throws(new ArgumentException("quantity cannot be less than 0"))
        .Assert();
}

So the state change was prevented and the invariants were guarded somewhere, but not in the right place. It fails here (note I am using AggregateSource by Yves Reynhout, this is synonymous with the Apply methods I’ve mentioned):

    private RoastDay()
    {
        ...

        Register<BatchAddedToRoastSequence>(@event =>
        {
            RoastSequence.Add(new Batch(@event.BatchId, // ArgumentException thrown here
                                        @event.BeanName,
                                        @event.BatchQuantity));
        });
    }

What if we already had some events written to the event store that would fail on the rehydration of the Aggregate Root? We can’t have this happen on rehydration, regardless of how confident we are that events already in the store are 100% valid, or that we can bend them into shape so that they are. The Apply methods should only have the responsibility of changing the state of the Aggregate, not throwing exceptions.

We need to be guaranteeing invariants have passed or failed here:

public void AddBatchToRoastSequence(Guid batchId, string beanName, decimal batchQuantity)
{
    ApplyChange(new BatchAddedToRoastSequence(batchId, beanName, batchQuantity));
}

But how can we do this? One way is that we can route the event from RoastDay to the Batch Entity and apply the event to the Batch for it to handle its invariants. The Batch Entity would look similar to our Aggregate Root:

public class Batch : Entity
{ 
    public BatchId Id { get; private set; }
    public string BeanName { get; private set; }
    public BatchQuantity Quantity { get; private set; }

    public Batch(Action<object> applier) : base(applier)
    {
        Register<BatchAddedToRoastSequence>(@event =>
        {
            Id = new BatchId(@event.BatchId);
            BeanName = @event.BeanName;
            Quantity = new BatchQuantity(@event.BatchQuantity);
        });
    }

    public static BatchAddedToRoastSequence PlanNewBatch(Guid batchId, string beanName, decimal quantity)
    {
        if (batchId == Guid.Empty) throw new ArgumentNullException(nameof(batchId), $"{nameof(batchId)} cannot be an empty Guid");
        if (quantity <= 0.00m) throw new ArgumentException("quantity cannot be less than 0");

        return new BatchAddedToRoastSequence(batchId, beanName, quantity);
    }
}

You can see here that instead of using a standard constructor with invariant checks, we are using the Entity class provided by AggregateSource, who’s base constructor we can pass an Action<object> applier) instance. Through this, we can share the ApplyChange for RoastDay with the Batch Entity, meaning when we call our Apply method (Register<BatchAddedToRoastSequence>), we will still have our @event recorded in the changes that have been applied to the RoastDay Aggregate Root. By routing the event to our Entity, we can still follow the rule that only ApplyChange should be throwing an exception. We can also follow the rule that we should leave any invariant protection out of the constructor used for rehydration.

public class Batch : Entity
{ 
    public BatchId Id { get; private set; }
    public string BeanName { get; private set; }
    public BatchQuantity Quantity { get; private set; }

    public Batch(Action<object> applier) : base(applier)
    {
        Register<BatchAddedToRoastSequence>(@event =>
        {
            Id = new BatchId(@event.BatchId);
            BeanName = @event.BeanName;
            Quantity = new BatchQuantity(@event.BatchQuantity);
        });
    }

    public static BatchAddedToRoastSequence PlanNewBatch(Guid batchId, string beanName, decimal quantity)
    {
        if (batchId == Guid.Empty) throw new ArgumentNullException(nameof(batchId), $"{nameof(batchId)} cannot be an empty Guid");
        if (quantity <= 0.00m) throw new ArgumentException("quantity cannot be less than 0");

        return new BatchAddedToRoastSequence(batchId, beanName, quantity);
    }
}

You can see that we are returning the event BatchAddedToRoastSequence from PlanNewBatch. This method is what we’re using to guard invariants before calling ApplyChange. The RoastDay Aggregate Root has its command handler method for AddBatchToRoastSequence, which when applied routes the calls Batch.PlanNewBatch on the Entity, which returns a BatchAddedToRoastSequence event itself. This event is then handled by the Batch Entity:

public class RoastDay : AggregateRootEntity
{
    public RoastDayId RoastDayId { get; private set; }
    public RoastDate RoastDate { get; private set; }
    public List<Batch> RoastSequence { get; private set; }

    private RoastDay()
    {
        ...

        Register<BatchAddedToRoastSequence>(@event =>
        {
            var batch = new Batch(ApplyChange);
            batch.Route(@event);
            RoastSequence.Add(batch);
        });
    }
    ...

    public void AddRoastSequenceToRoastDay()
    {
        ApplyChange(new RoastSequenceAddedToRoastDay());
    }

    public void AddBatchToRoastSequence(Guid batchId, string beanName, decimal quantity)
    {
        ApplyChange(Batch.PlanNewBatch(batchId, beanName, quantity));
    }

Our earlier spec verifying that the ArgumentException("quantity cannot be less than 0") was thrown still passes, but the exception is thrown here:

public static BatchAddedToRoastSequence PlanNewBatch(Guid batchId, string beanName, decimal quantity)
{
    if (batchId == Guid.Empty) throw new ArgumentNullException(nameof(batchId), $"{nameof(batchId)} cannot be an empty Guid");
    if (quantity <= 0.00m) throw new ArgumentException("quantity cannot be less than 0"); 
    // ArgumentException thrown here

    return new BatchAddedToRoastSequence(batchId, beanName, quantity);
}

Here’s a spec to verify the happy path, where BatchAddedToRoastSequence was fired when the invariants pass:

[Fact]
public void Should_accept_valid_addbatch_command()
{
    var batchId = Guid.NewGuid();
    new CommandScenarioFor<RoastDay>(RoastDay.Factory)
        .Given(new RoastSequenceAddedToRoastDay())
        .When(sut => sut.AddBatchToRoastSequence(batchId, "Guatemala Acatenango Gesha Lot 2", 1.00m))
        .Then(new BatchAddedToRoastSequence(batchId, "Guatemala Acatenango Gesha Lot 2", 1.00m))
        .Assert();
}

Summary

We were faced with the question of how to deal with non-Aggregate Root events, like those that need to be handled by child Entities of our Aggregate Root. We wanted to stick to some rules like 1) Event apply methods should only set properties without ever throwing an exception and 2) we shouldn’t be checking invariants when rehydrating an Aggregate Root from events.

We introduced a pattern where we route events to the child Entity, using a constructor that doesn’t check for invariants but instead calling a command handler method on the Entity which returns the event after checking invariants. This keeps the invariant checking out of the Entity constructor used for rehydration and allows for the definition of valid conditions on the Entity rather than on the Aggregate Root.

We could also use this pattern to route other events to our Entities. Aggregate Source gives us a convenient way to do so with the Route(@event) method.

Be sure to click the red button below to sign up for my mailing list. I have much more content queued up to be published, including a brand new book that puts a lot of this information in one place for you to use as a resource. Stay tuned!

Tweet
comments powered by Disqus