MessageHandler MH Sign Up

Aggregate Root

A

Introduction

Software systems need to take decissions.

Whether and how to respond to user commands are decissions that will influence the flow of a business process.

In this lesson, you'll implement an Aggregate Root pattern, which is the pattern assigned to take such decissions in a distributed system.

By the end of this part of the learning path, you will:

  • Understand what the responsibility of an Aggregate Root is.
  • Be able to identify the pattern from an Event Model.
  • Have built, and tested, an implementation of the pattern using MessageHandler.

What is an Aggregate Root

An Aggregate Root is the root of an aggregate.

An aggregate is a cluster of domain objects that is treated as a single unit.

Any references from outside the aggregate should only go to a specific entry point to the aggregate, the root, so that root can ensure the integrity of the aggregate as a whole.

As an aggregate root is responsible for maintaining integrity of the whole, it is the primary responsible for deciding if a command can be executed on the aggregate or not.

Aggregate Root

These decissions can be recorded as an event on an event stream, while any internal state of the aggregate can be derived from the recorded events.

Identifying Aggregate Roots

To identify an Aggregate Root in an Event Model, you search for the transitions between command (blue post-it) and event (orange post-it).

Each event is the result of a decission taken by the software.

Order booking
High resolution version

In your e-commerce system, when the buyer places a purchase order, the software could decide to either:

  • Start the booking process
  • Reject the order (e.g. for quantities less than 1)

Similar, when the seller wants to confirm the order, it could decide to either:

  • Confirm the order
  • Refuse to confirm the order (e.g. the order got withdrawn in the mean time)

When decissions depend on each other, and cannot exist as an autonomous component on their own, they need to be handled by the same Aggregate Root instance.

In this case, the decission to confirm an order cannot be taken without deciding that the order booking process could be started first.

So these decissions need to stay together, guarded by an Aggregate Root, called Order Booking (using the same name as the swimlane).

Setting up a new solution

Before you can write any code with Visual Studio, you first need to set up a solution, and project structure, to hold said code.

In this first part of the lesson, you'll set up a typical solution structure.

  • OrderBooking: Which will hold the aggregate root
  • OrderBooking.Events: Which will hold the events
  • OrderBooking.UnitTests: Here you can add tests to experiment with your domain model.

Create a solution with project to hold the events

  • Open Visual Studio 2022 Community Edition and select Create a new project.
  • Select the project template called Class Library, tagged with C#. Click Next.
  • Enter project name OrderBooking.Events.
  • Specify a desired location on disk where the files should be stored.
  • Enter solution name MessageHandler.LearningPath. Click Next.
  • Select framework .NET 6.0 (Long-term support). Click Create

Add a package reference to MessageHandler.EventSourcing.Contracts

  • Right click on Dependencies of the OrderBooking.Events project in the Solution Explorer and choose Manage Nuget Packages.
  • In the Nuget Package Manager window, browse for MessageHandler.EventSourcing.Contracts (include prerelease needs to be checked).
  • Select the latest version and click Install.
  • Click I Accept in the License Acceptance window

Add a project to hold the aggregate root

  • Right click the Solution MessageHandler.LearningPath in the Solution Explorer and choose Add > New Project.
  • Select the project template called Class Library, tagged with C#. Click Next.
  • Enter project name OrderBooking. Click Next.
  • Select framework .NET 6.0 (Long-term support). Click Create

Add a package reference to MessageHandler.EventSourcing

  • Right click on Dependencies of the OrderBooking project in the Solution Explorer and choose Manage Nuget Packages.
  • In the Nuget Package Manager window, browse for MessageHandler.EventSourcing (include prerelease needs to be checked).
  • Select the latest version and click Install.
  • Click I Accept in the License Acceptance window.

Add a project reference to OrderBooking.Events

  • Right click on Dependencies of the OrderBooking project in the Solution Explorer and choose Add Project Reference.
  • In the Reference Manager window, select OrderBooking.Events. Click OK

Add a project to hold unit tests

  • Right click the Solution MessageHandler.LearningPath in the Solution Explorer and choose Add > New Project.
  • Select the project template called xUnit Test Project, tagged with C#. Click Next.
  • Enter project name OrderBooking.UnitTests. Click Next.
  • Select framework .NET 6.0 (Long-term support). Click Create

Add a project reference to OrderBooking

  • Right click on Dependencies of the OrderBooking.UnitTests project in the Solution Explorer and choose Add Project Reference.
  • In the Reference Manager window, select OrderBooking. Click OK

Solution structure overview

Congratulations, you've now reached the end of the solution setup part.

The solution structure now consists of three projects:

  • OrderBooking: Will hold the aggregate root
  • OrderBooking.Events: Will hold the events
  • OrderBooking.UnitTests: Here you can add tests to experiment with your domain model.

The solution structure created up until now can also be found in the 01 start folder of the learning path reference repository.

Implementing the Aggregate Root

Now that you have the solution set up, it's time to start implementing the aggregate root.

You're going to do it Test Driven Development (TDD) style, which means you use unit tests to drive the design of your aggregate root, and the domain model encapsulated in it.

Writing a test

Tests are created in the OrderBooking.UnitTests project.

First you rename the Class1.cs file in the OrderBooking.UnitTests project to WhilePlacingPurchaseOrders.cs.

Visual Studio will likely suggest to rename the class Class1 to WhilePlacingPurchaseOrders in the project automatically, you can allow it to do so. Otherwise you can change the class name manually.

Rename the method Test1 to reflect the scenario under test GivenNewBookingProcess_WhenPlacingValidPurchaseOrder_TheBookingProcessShouldHaveBeenStarted.

Your code now looks like this:

using Xunit;

namespace OrderBooking.UnitTests
{
    public class WhilePlacingPurchaseOrders
    {
        [Fact]
        public void GivenNewBookingProcess_WhenPlacingValidPurchaseOrder_TheBookingProcessShouldHaveBeenStarted()
        {

        }
    }
}

Once this test skeleton is in place, you can add code to it.

Code which defines the public API of your aggregate root for the scenario at hand.

In this scenario you start with a new OrderBooking process.

You invoke the PlacePurchaseOrder command on it, passing in the PurchaseOrder details.

Assuming the PurchaseOrder instance is valid, you expect the BookingStarted event to exist in the commit history.

using System.Linq;
using Xunit;

public class WhilePlacingPurchaseOrders
{
    [Fact]
    public void GivenNewBookingProcess_WhenPlacingValidPurchaseOrder_TheBookingProcessShouldHaveBeenStarted()
    {
        // given
        var booking = new OrderBooking();

        //when
        var purchaseOrder = new PurchaseOrder();
        booking.PlacePurchaseOrder(purchaseOrder);

        //then
        var pendingEvents = booking.Commit();
        var bookingStarted = pendingEvents.FirstOrDefault(e => typeof(BookingStarted).IsAssignableFrom(e.GetType()));

        Assert.NotNull(bookingStarted);
    }
}

This code will not compile yet, you still have to define the classes used in the scenario.

Coding the domain model

The first class to create is OrderBooking, your aggregate root.

Rename the file Class1.cs to OrderBooking.cs in project OrderBooking.

Visual Studio will likely suggest to rename the class Class1 to OrderBooking in the file automatically, you can allow it to do so. Otherwise you can change the class name manually.

Given this will be an event sourced entity, inherit it from EventSourced and provide the required constructor.

using MessageHandler.EventSourcing.DomainModel;

namespace OrderBooking
{
    public class OrderBooking : EventSourced
    {
        public OrderBooking() : this(Guid.NewGuid().ToString())
        {
        }

        public OrderBooking(string id) : base(id)
        {
        }
    }
}

OrderBooking will be the root for an aggregate, which is a cluster of domain objects.

Among them there is a class representing the PurchaseOrder.

Right click on the OrderBooking project in the Solution Explorer and choose Add > New Item.

In the Add New Item window select Class and name it PurchaseOrder.cs.

You pass the PurchaseOrder into the aggregate, through the root, by creating a method for the PlacePurchaseOrder command.

public class OrderBooking : EventSourced
{
    public OrderBooking() : this(Guid.NewGuid().ToString())
    {
    }

    public OrderBooking(string id) : base(id)
    {
    }

    public void PlacePurchaseOrder(PurchaseOrder purchaseOrder)
    {
        throw new NotImplementedException();
    }
}

At this point, your code still does not compile, you still need to define the expected event model.

Coding the BookingStarted event

Rename the file Class1.cs to BookingStarted.cs in project OrderBooking.Events.

Visual Studio will likely suggest to rename the class Class1 to BookingStarted in the file automatically, you can allow it to do so. Otherwise you can change the class name manually.

As this is an event that needs to be sourced from an event store, inherit it from SourcedEvent.

using MessageHandler.EventSourcing.Contracts;

public class BookingStarted : SourcedEvent
{

}

Given this event is defined in the OrderBooking.Events namespace, you need to add the following using statement to the test WhilePlacingPurchaseOrders

using OrderBooking.Events;

Now your code compiles, finally!

Run the test

Right click the OrderBooking.UnitTests project in the Solution Explorer and choose Run Tests.

Your test should FAIL due to a System.NotImplementedException!

The reason is that the command method PlacePurchaseOrder has not been implemented yet.

Coding the command method

In order to satisfy the test, you emit the BookingStarted event from the command method.

public class OrderBooking : EventSourced
{
    public OrderBooking() : this(Guid.NewGuid().ToString())
    {
    }

    public OrderBooking(string id) : base(id)
    {
    }

    public void PlacePurchaseOrder(PurchaseOrder purchaseOrder)
    {
        Emit(new BookingStarted());
    }
}

Run the test again, and it will SUCCEED! Congratulations!

Protecting invariants of the aggregate

However, the PlacePurchaseOrder command method has the responsibility to guard the integrity of the aggregate it is protecting.

This means that any command method should start with a check, before emitting a decission.

Usually, this check needs to be based on previous decissions though.

For example, placing the same order twice, should not result in starting two booking processes. This requirement is often called idempotency.

You represent it with a test.

[Fact]
public void GivenNewBookingProcess_WhenPlacingValidPurchaseOrderTwice_TheBookingProcessShouldHaveBeenStartedOnlyOnce()
{
    // given
    var booking = new OrderBooking();

    //when
    var purchaseOrder = new PurchaseOrder();
    booking.PlacePurchaseOrder(purchaseOrder);
    booking.PlacePurchaseOrder(purchaseOrder);

    //then
    var pendingEvents = booking.Commit();
    var bookingStarted = pendingEvents.Where(e => typeof(BookingStarted).IsAssignableFrom(e.GetType()));

    Assert.Single(bookingStarted);
}

Running this test, you notice that it FAILS with message.

The collection was expected to contain a single element, but it contained more than one element.

To fix it, you need to modify the PlacePurchaseOrder command method.

Some in memory state needs to be kept to remember if the process started before or not.

You derive this by applying historical events.

To achieve this requirement, you implement IApply<T> for the BookingStarted event to set a field _allreadyStarted to true and you check this field at the start of the PlacePurchaseOrder command method to prevent duplicate execution.

public class OrderBooking : EventSourced,
        IApply<BookingStarted>
{
    private bool _allreadyStarted;

    public OrderBooking() : this(Guid.NewGuid().ToString())
    {
    }

    public OrderBooking(string id) : base(id)
    {
    }

    public void PlacePurchaseOrder(PurchaseOrder purchaseOrder)
    {
        if(_allreadyStarted) return;

        Emit(new BookingStarted());
    }    

    public void Apply(BookingStarted msg)
    {
        _allreadyStarted = true;
    }
}

Now, you run the test again and see it SUCCEED.

Event carried state transfer

You notice one more problem in your design: that precious (multi million dollar!) PurchaseOrder is ignored after passing it in into PlacePurchaseOrder and you don't know who placed the order.

It is also the aggregate roots responsibility to hold on to all the internal state it is protecting, or the data would be lost.

This does not mean the root needs to have a private member field for all the data in the aggregate!

It should ensure that all the data is available on the event stream so that it can be pulled from the events, if needed in the future.

This concept is called, Event carried state transfer.

You add a test to ensure, that the PurchaseOrder and name of the buyer passed in, also gets added to the BookingStarted event.

[Fact]
public void GivenNewBookingProcess_WhenPlacingValidPurchaseOrder_ThenPurchaseOrderIsCarriedOnEvent()
{
    // given
    var booking = new OrderBooking();

    //when
    var purchaseOrder = new PurchaseOrder();
    booking.PlacePurchaseOrder(purchaseOrder, "Mr. Buyer");

    //then
    var pendingEvents = booking.Commit();
    var bookingStarted = pendingEvents.FirstOrDefault(e => typeof(BookingStarted).IsAssignableFrom(e.GetType()));

    Assert.NotNull(bookingStarted.BookingId);
    Assert.NotNull(bookingStarted.PurchaseOrder);
    Assert.NotNull(bookingStarted.Name);
}

To make the code compile, you add a PurchaseOrder and Name property to the BookingStarted event.

To avoid unnecessary code duplication, you move PurchaseOrder.cs from the OrderBooking project to the OrderBooking.Events project instead of defining it a second time.

It's also usefull to add an identifier for the process at hand to the events, so you also add a BookingId property.

public class BookingStarted : SourcedEvent
{
    public string BookingId { get; set; }

    public string Name { get; set; }

    public PurchaseOrder? PurchaseOrder { get; set; }
}

Now you run the test to see it FAIL, because the PurchaseOrder and Name properties are null.

Which you fix, by assigning the PurchaseOrder and Name property in the PlacePurchaseOrder command method.

public class OrderBooking : EventSourced,
    IApply<BookingStarted>
{
    private bool _allreadyStarted;

    public OrderBooking() : this(Guid.NewGuid().ToString())
    {
    }

    public OrderBooking(string id) : base(id)
    {
    }

    public void PlacePurchaseOrder(PurchaseOrder purchaseOrder, string name)
    {
        if (_allreadyStarted) return;

        Emit(new BookingStarted()
        {
            BookingId = Id,
            Name = name,
            PurchaseOrder = purchaseOrder
        });
    }

    public void Apply(BookingStarted msg)
    {
        _allreadyStarted = true;
    }
}

You run the test again to see it SUCCEED.

Well done! You made it to the end of the lesson!

The code created up until now can also be found in the 02 aggregate root folder of the learning path reference repository for future reference.

Summary

In this lesson you learned how to build an Aggregate Root, one of the most common processing patterns used for building distributed software.

What’s coming next?

In Part 4 of the Tutorial, you’ll implement another one of the common patterns: a Projection.

TO PART 4

Sign up to our newsletter to get notified about new content and releases

You can unsubscribe at any time by clicking the link in the footer of your emails. I use Mailchimp as my marketing platform. By clicking subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.