Clean up your Domain Model with Event Sourcing

Vacuum cleaner - Workers collectionI recently had a discussion with a developer who was new to Domain Driven Design, CQRS and Event Sourcing.  They were using Greg Young’s Simple CQRS Example to create there own simple app as learning exercise.

After taking a quick look at their aggregate root, one thing that immediately stood out to me was a class field that contained current state.  Why this is interesting is because this field was not in any invariant.

When I first started reading and exploring Event Sourcing, this was a big “ah ha!” moment for me.

The realization that only certain data within my domain model was really important.

We will use a simple shopping cart example.

First, lets look at what our shopping cart would look like without being event sourced. We would be probably persisting this to a relational database using an ORM or perhaps storing it in a NoSQL document store.

public class ShoppingCart
{
    public Guid Id { get; set; }
    public IList<ShoppingCartLineItem> LineItems { get; set; }
    public bool IsComplete { get; set; }
    public DateTime CompleteDate { get; set; }

    public ShoppingCart(Guid id)
    {
        Id = id;
        LineItems = new List<ShoppingCartLineItem>();
    }

    public void AddProduct(string productSku, int quantity)
    {
        if (string.IsNullOrEmpty(productSku)) throw new ArgumentException("productSku");
        if (quantity <= 0) throw new ArgumentException("quantity");

        LineItems.Add(new ShoppingCartLineItem(productSku, quantity));
    }

    public void Complete()
    {
        if (IsComplete) throw new InvalidOperationException("Already completed.");
        if (LineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products.");

        IsComplete = true;
        CompleteDate = DateTime.UtcNow;
    }
}

There are a few things to point out in the above code.

Take note of the public properties which represent our current state.  These are the pieces of data that we be persisted to data storage.

Notice we have a IList<ShoppingCartLineItem> which is our collection of products that are in our shopping cart.  The ShoppingCartLineItem class contains the Product SKU and the quantity ordered.

One invariant in this example to mention is in the Complete() method.  There must be at least one product added to the shopping cart in order to mark it as complete.

if (LineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products.");

Next, let’s implement the above with event sourcing.

public class ShoppingCart : AggregateRoot
{
    private Guid _id;
    private bool _completed;
    private DateTime _completedDate;
    private readonly IList _lineItems;
 
    public override Guid Id
    {
        get { return _id; }
    }

    private void Apply(ShoppingCartCreated e)
    {
        _id = e.Id;
    }

    private void Apply(ProductAdded e)
    {
        _lineItems.Add(new ShoppingCartLineItem(e.ProductSku, e.Quantity));
    }

    private void Apply(Completed e)
    {
        _completed = true;
        _completedDate = e.DateCompleted;
    }

    public ShoppingCart(Guid id)
    {
        _lineItems = new List();

        ApplyChange(new ShoppingCartCreated(id));
    }

    public void AddProduct(string productSku, int quantity)
    {
        if (string.IsNullOrEmpty(productSku)) throw new ArgumentException("productSku");
        if (quantity <= 0) throw new ArgumentException("quantity");

        ApplyChange(new ProductAdded(_id, productSku, quantity));
    }

    public void Complete()
    {
        if (_completed) throw new InvalidOperationException("Already completed.");
        if (_lineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products.");

        ApplyChange(new Completed(_id, DateTime.UtcNow));
    }
}

In our event sourced implementation above we still are holding current state in the same fashion as our non event sourced version.  I’ve changed them from public properties to private fields since they are not being persisted to data storage.  Instead our events raised through ApplyChange() are what are persisted to data storage.

The example above is typical of a first implementation of event sourcing.  We are taking the concept we know and use of persisting current state along with this new idea of using events as state transitions.

There is actually a lot of code that can be removed from our event sourced example above.

ShoppingCartLineItem class is no longer needed because the data it encapsulates is now encapsulated in our ProductAdded event.  So if we delete that class, then we have the issue with:

private readonly IList<ShoppingCartLineItem> _lineItems;

This private member will no longer be valid since we just deleted the ShoppingCartLineItem class.  So we can remove this private member as well.

Now let’s go back to that invariant that we have:

if (_lineItems.Any() == false) throw new InvalidOperationException("Cannot complete with no products.");

How can we maintain this invariant since we are no longer keep track of the line items?

You only need the data applicable to an invariant

Because of this, we don’t really care what products or quantities are on the shopping cart, just that there is at least one.

Any data that is not used by an invariant does not need to be in your aggregate.

private DateTime _completedDate;

We also no longer need the date the shopping cart was completed because it is not used by an invariant and it is also contained in the Completed event.

Now let’s take a look at our shopping cart with these ideas applied:

public class ShoppingCart : AggregateRoot
{
    private Guid _id;
    private bool _containsProducts;
    private bool _completed;

    public override Guid Id
    {
        get { return _id; }
    }

    private void Apply(ShoppingCartCreated e)
    {
        _id = e.Id;
    }

    private void Apply(ProductAdded e)
    {
        _containsProducts = true;
    }

    private void Apply(Completed e)
    {
        _completed = true;
    }

    public ShoppingCart(Guid id)
    {
        ApplyChange(new ShoppingCartCreated(id));
    }

    public void AddProduct(string productSku, int quantity)
    {
        if (string.IsNullOrEmpty(productSku)) throw new ArgumentException("productSku");
        if (quantity <= 0) throw new ArgumentException("quantity");

        ApplyChange(new ProductAdded(_id, productSku, quantity));
    }

    public void Complete()
    {
        if (_completed) throw new InvalidOperationException("Already completed.");
        if (_containsProducts == false) throw new InvalidOperationException("Cannot complete with no products.");

        ApplyChange(new Completed(_id, DateTime.UtcNow));
    }
}

The invariant is now based on a boolean set when a product has been added.  The aggregate does not care about what products were added or their quantity.  It doesn’t even need to have them in a collection.

With the implementation of event sourcing in our aggregate, we get a much clearer view of what data drives business logic and is important in our domain model.

3 thoughts to “Clean up your Domain Model with Event Sourcing”

  1. Hi Derek,

    first of all thx for ur very valuable post !

    Tho i’m wondering about this :
    ” The aggregate does not care about what products were added or their
    quantity. It doesn’t even need to have them in a collection.”

    For your example’s purpose, i well understand it.
    But i can’t imagine any domain expert not requiring a TransformShoppingCartToOrder(Guid ShoppingCartId) Command.
    If you dont’t keep a trace of the added products, how would this work?
    You’d need again your “private readonly IList _lineItems;” right?

    Again i’m not asking this because your example is unclear, it is clear. And could be well summed up by your assertion “Any data that is not used by an invariant does not need to be in your aggregate.”

    I’m asking this to understand if I’m in front of a over-simplified example serving only one purpose (which then would be ok) or if that’s a sign that I’m still missing some important concepts (which for me would be less OK 🙂 )

    Thx for ur post again !

    1. Yes, you are correct. This is an over simplified example. You are right on the money with your comments.

      If your ShoppingCart did look like how i posted it, and then you did need to add a TransformShoppingCartToOrder command, you would be then be refactoring your ShoppingCart to have a collection of ShoppingCartLineItems like you mention. That wouldn’t be much of a challenge since you would have the data needed in your events to do so.

      As a matter of fact, I’ve actually been going thru this on a personal project I’m working on. Where I modeled my aggregate, and was only concerning myself with a specific set of state/data encapsulated within for a set of business rules. Then I realized that I had a new behavior that required a new business rule. That business rule was easy to implement, but it meant I had to change how i was handling state within my aggregate.

      Thanks for the comment!

      1. Hi Derek and thx very much for ur very useful reply!

        The newbie that I am is glad to see he got right 🙂

        If you have time, i posted a question on SO reguarding the design of an Aggregate Root. Feel free to help goo.gl/3n5SWv or to delete this post if you consider it’s polluting your blog

        Thx again for ur reply and for this blog!

Leave a Reply

Your email address will not be published. Required fields are marked *