What rules to follow when designing aggragates in DDD?

382 Views Asked by At

I am re-designing my side-project to utilize DDD. I am doing this for learning purposes. It's an application for planning home budget and analysis of spendings. One of functionalities of the app is that user registers expenses and divides them into categories.

I have general question: how do you design aggregates? What steps to follow?

Below you'll find steps that I followed that lead me nowhere.

  1. I did design-level event storming session for the project up to a step where I have identified invariants and now I am trying to name aggregates. Please consider following slice of event storming artifact as an example: enter image description here

  2. I identified relevant entities. Entities relevant to the example:

  • Expense
  • Expense category
  • Expense category group
  1. I designed aggregate that fulfills all of the invariants: enter image description here

  2. I read this great article about designing aggregates. According to the article aggragates should follow the rules of:

  • Consistency of the lifecycle
  • Consistency of the problem domain
  • Consistency of the scenario frequency
  • As few elements as possible within the aggregation

In case of my aggregate I can see that:

  • Consistency of lifecycle rule is violated (because expense is still meaningful when you delete expense category)
  • Consistency of the scenarion frequency rule is violated (because expenses will created much more frequent than expense categories will be modified)
  • There's also to many elements in the aggregate. The expenses list will be growing.
  1. I re-designed the aggregates so that the rules are satisfied. Here's what I've got. enter image description here

  2. I realized that now one of the invariants is not part of transactional consistency. Namely the invariant stating "Expense cannot be assigned to category withdrew from usage before the expense date". I know that it is possible to negotiate business rules and replace invariant with some sort of corrective policy but in this case I have no idea of what this policy can be (this is side-project, I am the stakeholder).

And now I am stuck. Please, help. What am I doing wrong?

So far my conclusion are that:

  • sometimes I can't have small and well-designed aggregates that satisfy all requirements on consistency
  • DDD style application will probably degenerate fast when developed by team with usual structure (more regular/junior developers than seniors/leaders).
  • developing DDD style adds huge overhead spent on analysis of which rules should be transactionally consistent, which eventually consistent, how changes to rules impact aggregate structure
1

There are 1 best solutions below

10
Neil W On

Your conclusions are reasonable.

Regarding the invariant:

Expense cannot be assigned to category withdrew from usage before the expense date

I presume you have a method on your Expense that accepts a Category Id and amount, so that this can be called to categorise your expense.

I'd pass in the Category entity itself and go for:

public class Category
{
    DateTime? Withdrawn { get; set; }

    public bool IsWithdrawn() => Withdrawn != null && Withdrawn < DateTime.Now;
}

public class Expense
{
    public void Categorise(Category category, decimal amount)
    {
        if (category.IsWithdrawn())
        {
            throw new InvalidOperation("Cannot use category.  It is withdrawn.");
        }

        // Complete the categorisation
    }
}

Now, your application layer will retrieve the Category to be passed into the Expense method.

The Expense can then enforce its own invariant.

NOTE:

There is the corner case that a Category gets withdrawn by another process after your application layer has retrieved it but before your Expense is committed. As this end-to-end process would be very short, my guess is that this is unlikely to be a concern in your use case, but worth considering.

Approaches for ensuring that Category has not been 'withdrawn' before Expense is saved:

1 - Use a Domain Event

When the Expense is categorised add an appropriate domain event. The domain event handler can then perform a just-in-time check about the validity of the Category before the transaction is committed.

2 - Catch FK Exception

If 'withdrawn' means deleted from the database, then use the approach from above and when you try to save the Categorisation with an FK pointing to a now-deleted Category then you'll get an FK exception which you can catch and handle.

3 - Use Concurrency Token

If 'withdrawn' is just a flag that's put on the Category or Category is not referenced thru a database-enforced foreign key, then you could use a concurrency token. Various ways of implementing it, but using this approach will tell you if the state of the Category may have changed since you last retrieved it. If so, you can re-run the command. If that state change was the 'withdrawal' of the category, then second time around the approach above will enforce the invariant.