DDD Aggregates – Multiple Views of Same Concept

aggregatedomain-driven-design

We're completely remodelling a system at the company for which I am currently working. We're applying DDD and for the very first time I have actually got someone on my team who has some prior experience with DDD as well (yay!)

This new system is extremely user-centric, i.e. pretty much every operation within the system comes from the end-user. Some of these operations may be:

  1. Create an Account. A non-paying user is limited to one active account, pro-members can have as many accounts as they want.
  2. Add a Transaction to an Account, but only if a User has access to said Account.
  3. Reorder Divisions in an Account, but only if a User has access to said Account.

There are a lot of these rules, all point to a single user and this is where me and my colleague came into argument.

My opinion is, aggregates should be as small as necessary, as cohesive, this makes their testing and understanding easier. E.g. if I model the first rule, I would create a simple aggregate, which could look like this:

class UserWithActiveAccounts {

    private UserId id;
    private NonNegativeNumber countOfActiveAccounts;
    private MembershipType membershipType;

    public Account createNewAccount(AccountId accountId, NonEmptyString accountName) {
        if (
            MembershipType.FREE.equals(membershipType) &&
            countOfActiveAccounts.GT(NonNegativeNumber.fromValue(1))
        ) {
            throw AccountLimitExceeded.forFreeUserTooManyAccounts(id);
        }

        return Account.withOwner(accountId, accountName, id);
    }
}

This aggregate contains only count of currently active accounts and user's membership type, because that's what the business rule when creating a new account cares about.

Then, if I had to model the other 2 user cases, I would need another aggregate, such as UserWithAccessibleAccounts, which would internally contain a List of AccountId values, which I could iterate upon adding a Transaction with a specific AccountId to check, whether the operation is allowed or not.

class UserWithAccessibleAccounts {

    private UserId id;
    private List<AccountId> accessibleActiveAccounts;

    public Transaction addTransaction(
        TransactionId transactionId,
        AccountId toAccountId,
        Number value
    ) {
        if (hasAccessToAccount(toAccountId)) {
            throw CannotAddTransaction.noAccessToAccount(id, toAccountId);
        }

        return Transaction.byUserInAccount(transactionId, value, id, toAccountId);
    }

    private bool hasAccessToAccount(AccountId accountId) {
        return accessibleActiveAccounts.contains(accountId);
    }
}

Obviously, with my approach you will have a lot of small aggregates each responsible for a tiny piece of business rule.

What my colleague would like is to have a large class, such as User, which would contain all operations. Which I am against. I believe it would lead to a god messy class, where it would be much more difficult to track bugs. Also, as this class would grow, each time you would want to run any of the operations on the user, you would need to load a lot of properties which are unrelated to the operation that the user is currently trying to do (such as loading the List of AccountId values when a simple count of them is necessary, in this very simplified example).

We're basically trying to solve the problem of:

  • either having to come up with a lot of class names for all those small aggregates,
  • deal with scalability issue and a humongous class due to pulling all the data for each operation, no matter how small.

I am inclined to the small aggregates approach, but perhaps I am not looking at it from the correct angle and there's something about the approach my colleague is suggesting I am simply not seeing.

Best Answer

First of all. If you want to follow DDD then one of the rules for creating aggregates is that the transactions should not cross aggregate boundaries. But in your approach if a UserWithActiveAccount has a new accessible account then you will have to delete UserWithActiveAccount and create a new UserWithAccessibleAccount in the same transaction. Which is breaking one of the rules of designing aggregates in DDD.

I can see why you might think it is better to create different user entities. But be aware that you are now creating entities that doesn't even exist in your domain. And this can easily go wrong at one point. You can easily end up with a big number of user classes that doesn't exist in the ubiquitous language and then the amount of cognitive load (information needed to understand the code) will be huge.

Think about having more possible states for the user(user with expired account, user with deleted account ...). You will have to create a new class for each one and then what about a user that can be in two states at the same time? Would create a new class for each combination?

DDD is all about designing a model that reflects the domain (business) and in your domain there is a user that can have different states but it is still the same user. So I would suggest to have the same in the code. That means a user entity that can have different states.

Now let's discuss what can go wrong with this approach:

aggregates should be as small as necessary, as cohesive, this makes their testing and understanding easier.

This is not necessarily true. Keeping the functions small and following some of the coding good practices will keep the class simple and easy to understand. The size of the class doesn't necessarily mean it is complicated.

Also, as this class would grow, each time you would want to run any of the operations on the user, you would need to load a lot of properties which are unrelated to the operation that the user is currently trying to do (such as loading the List of AccountId values when a simple count of them is necessary, in this very simplified example).

What is the problem with that? Do you have limited memory on the machine where this code will be executed? will this result in a noticeable decrease in performance? If not then there is no reason to do some premature optimization for problem that you will probably never have to deal with. If all the data is well encapsulated in your class, there is no problem if it contains many properties.

Related Topic