How *big* is an Aggregate?

tldr; at the end of this post.

A question that comes up often when designing aggregates: should an Aggregate load all of its children?

Let’s look at a simple example:

  1. A Customer has an Account
  2. A Customer makes payments into their Account
  3. Over time, an Account may have hundreds of individual Payments

There appears to be a conflict between these facts and the tenets of DDD:

  • An Aggregate encapsulates related Entities within it
  • Aggregates should be as small as possible

But:

  • Payments must be added via the Account aggregate. Therefore, the main aggregate must be loaded first.
  • An Account may already contain hundreds of Payments. It may be huge when we load it into memory.

So how can an Aggregate be small, yet contain many items at the same time?

If you take a step back, there are actually 2 requirements at play: operational and structural.

The operational (aka command/transaction) requirement is in the “Payment Processing” context.  Here, the Account aggregate (AR) is as small as possible. It contains a Balance, and maybe a reference to the Last Known Payment. When a new Payment is made, the AR can be loaded, modified, and saved very quickly.

The structural (aka query) requirement would apply to the “UI context”.  Here, the UI provides access to the history of payments. No transactions are performed against this Aggregate.

So how does the UI access the Payment history?  There are a few options:

  1. Use a separate store for all Payments
  2. Call a GET API on the “Payment Processing” context
  3. The “Payment Processing” context could raise a Domain Event that the “UI Context” listens to, which in turn updates its own local database. This way, the UI doesn’t have to call an external API.

Simulating this in Fresnel

Both requirements may be achieved through the Account aggregate. Let’s deal with the operational requirement first:

Create a Command that adds a Payment to an Account:

public class AddNewPaymentCommand : ICommand
{
    ...
        public object? Execute()
        {
            if (_Account == null)
                return null;

            var newPayment = new Payment
            {
                Id = Guid.NewGuid(),
                Amount = Amount,
                PaymentDate = PaymentDate,
                ParentAccount = 
                    AggregateReference<Account>.From(_Account)
            };

            _Account.Payments.Add(newPayment);

            return newPayment;
        }
    ...
}

Then expose the AddNewPaymentCommand on the Account aggregate

public class Account : IAggregateRoot
{
    ...
    public AddNewPaymentCommand AddNewPayment()
    {
        return new AddNewPaymentCommand(this);
    }
    ...
}

When the action is triggered in the UI, the operation executes without loading any existing payments:

Next we’ll handle the structural/query side:

Create a Query Specification that returns Payments for an Account:

// A Query that returns the Payments for an Account
public class AccountPaymentsQuerySpecification : IQuerySpecification<Payment>
{
    ...
    public IEnumerable<Payment> GetResults(Account account)
    {
        return
            _PaymentRepo.Payments
            .Where(p => p.AccountRef.AggregateId == account.Id);
    }
    ...
}

Then use lazy-loading on the Account aggregate:

public class Payment: IAggregateRoot
{
   ...

   [Relationship(RelationshipType.Owns]]
   [Ui(UiRenderOption.SeparateTabExpanded)]
   [RelationalQuery(typeof(PaymentAccountPaymentsQuerySpecification)]
   public ICollection<Payment> Payments { get; set; } = [];
   ...
}

Whenever the Payments collection is viewed in the UI, the data will be loaded on-demand:

tldr;

  • Aggregates should be as small as possible
  • Changes to the child objects must be coordinated by the Aggregate Root.
  • All changes to an Aggregate should be within a transaction
  • For large child contents, use queries to access those objects (and keep them read-only)
Scroll to Top