Skip to content

Clean up your Domain Model with Event Sourcing

Sponsor: Using RabbitMQ or Azure Service Bus in your .NET systems? Well, you could just use their SDKs and roll your own serialization, routing, outbox, retries, and telemetry. I mean, seriously, how hard could it be?

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


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.